GPIO 编号规则

F1C200s 有 5 个 PIO(平台 I/O)端口组,分别标记为 PIO0、PIO1、PIO2、PIO3 和 PIO4。
每个 PIO 端口组包含了 16 个 GPIO 管脚,分别标记为 PIOx.0 到 PIOx.15,其中 x 为 PIO 端口组的编号,从 0 到 4。

硬件制作

一、工作环境及项目简介

立创EDA:硬件原理图及PCB绘制。

全志F1C200S:F1C100S内置32MB DDR1内存,F1C200S内置64MB DDR1内存。

原理图:参考开源项目,详见墨云, 详见peng-zhihui

核心板:四层。

底板:两层。

工具:烙铁、热风枪、焊锡膏、洗板水、各种电子元器件。

二、原理图设计

1、核心板

🍎 电源电路

在F1C200s的datasheet Allwinner_F1C200s_Datasheet_V1_1中可以得到推荐的运行环境,主要参考Typ,也就是典型值:

因此我们需要实现的电压有四个:1.1V、2.5V、3.0V、3.3V。

使用SY8088AAC SOT-23-5同步降压DC-DC稳压器,设置其外围的电阻搭配调整其输出电压(Vout = 0.6 * (1+Ra/Rb) ),使用其实现1.1V、2.5V、3.3V三个电压需求:

使用XC6206P302MR-SOT23的封装方式,输入电压5V ,输出需求电压3.0V:

电路设计如下图所示,根据墨云所说电感选用2.2uH功率电感,电感额定电流为2A。此处的5V电源来自底板的USB供电。下面电阻的搭配方式可以进行调节,只要满足输出的电压为需求电压即可

🍎 板对板连接器

板对板连接器选用可靠性较高的排针排母对。

🍎 复位电路

复位电路比较简单,不再赘述。

🍎 晶振电路

晶振采用24MHz无源晶振,两个15pF电容滤波。

🍎 主控电路

主控电路主要参考墨云和稚辉君,相关链接见上方。主要从主控F1C200s中引出TF卡引脚(本文选用的系统加载电路)、音频、晶振、串口、复位、SPI、OTG、图像、一些GPIO等等。

2、底板

🍍 串口转USB电路

选用CH340E芯片,注意某宝的芯片可能是拆机、复新芯片,如果串口通信失败,考虑芯片问题。至于下面的接线,墨云提到:“根据CH340E官方的原理图,当VCC接入5V的时候,V3 需要接一个100nf的电容,但是此处在V3直接接入5V,也可以工作。”,因此本文选择将VCC接入5V,将V3接入3.3V。

🍍 TF卡电路

TF卡作为本系统唯一的系统加载方式,具体接线方式如下所示,TF卡为自弹minTF卡,只要PCB和你的硬件匹配,其他全部相同。

🍍 USB扩展电路

FE8.1(1扩4)是一个非常紧凑的高速4端口USB集线器控制器。此处我们仅使用其中两个,也就是将一个USB扩展为两个。

🍍 WIFI电路

主要参考墨云的接线方式,暂未进行验证。

🍍 TFT屏幕

1.14英寸的IPS屏幕,某宝十几块钱,我这里加了一个接线端子,因为我留出了4.3寸屏幕(正点原子)的接口,防止干扰,我加了接线端子进行选择。

🍍 音频

在墨云的基础上,结合稚辉君,我添加了咪头和3.5mm耳机接口。

🍍 板对板连接器

和核心版的板对板连接器相对应,注意不要搞反了,否则可能导致短路,板对板连接器管脚分布和PCB布局有关,因为我未考虑到PCB布局,导致后面的布局较为困难,虽然核心板采用四层板,底板采用两层板,但布线还是花了很大功夫。

🍍 40Pin4.3寸屏幕

屏幕为4.3寸正点原子屏幕,采用FPC连接,接线端子管脚间距为0.5mm,由于我的焊接设备原因,焊接较为麻烦。

三、PCB展示

Wood是我做的标志,之所以选用排针连接底板和核心板,是因为排针相较于金手指和BTB来说,可以重复拔插,更便于将一个核心板应用到其他底板上。

**核心板(四层,挺漂亮哈)** **底板**

四、实物展示

**核心板** **底板** **焊接完成组装实物**

uboot移植

一、前言

在移植Linux之前我们需要先移植一个bootloader代码,这个bootloader代码用于启动Linux内核,bootloader有很多,常用的就是 uboot。移植好uboot以后再移植Linux内核,移植完Linux内核以后Linux还不能正常启动,还需要再移植一个根文件系统(rootfs),根文件系统里面包含了一些最常用的命令和文件。所以*uboot、Linux kernel 和 rootfs* 这三者一起构成了一个完整的Linux系统,一个可以正常使用、功能完善的 Linux 系统。

– 来自正点原子嵌入式Linux驱动开发指南。

Linux系统要启动就必须需要一个bootloader程序,也就说芯片上电以后先运行一段bootloader程序。这段 bootloader 程序会先初始化DDR等外设,然后将 Linux 内核从 flash(NAND,NOR FLASH,SD,MMC等)拷贝到DDR中,最后启动Linux内核。下面我们首先进行uboot的移植,或者说适配。


二、F1C200s上电启动顺序

在进行uboot移植之前,需要了解F1C200s芯片上电后启动的顺序,避免重复造轮子,我们直接根据F1C100s启动时搜索SPI Flash的顺序?了解到启动顺序如下,F1C200s同F1C100s。

\1. 上电后, f1c100s内部 BROM (芯片内置,无法擦除) 启动,
\2. 首先检查 SD0 有没有插卡, 如果有插卡就读卡 8k偏移数据,是否是合法的启动数据, 如果是BROM 引导结束, 否则进入下一步
\3. 检测SPI0 NOR FLASH(W25QXXX, MX25LXXX) 是否存在, 是否有合法的启动数据, 如果是BROM 引导结束, 否则进入下一步
\4. 检测SPI0 NAND FLASH 是否存在, 是否有合法的启动数据, 如果是BROM 引导结束, 否则进入下一步
\5. 因为找不到任何可以引导的介质, 系统进入usb fel 模式, 可以用USB烧录了。

上电之后,F1C200s芯片内部的BROM启动检查到SD卡,读取8k偏移数据。

注意:这里的bs=1024 seek=8是添加了8192字节的偏移,之所以要加8K偏移是因为FSBL也就是bootROM里面硬写死了会从设备的8K地址处加载SPL,然后进入uboot。因此上面烧写的时候,指定的偏移地址一定是相对于储存设备硬件的偏移,而不是相对于分区的偏移!

来自:peng-zhihui/Planck-Pi: Super TINY & Low-cost Linux Develop-Kit Based On F1C200s.


三、前期准备

虚拟机VmwareWorkstation安装;

ubuntu16.04系统安装。


四、新建用户

如果你已经再当前用户安装了一些其他的编译器,那么最好创建一个新的账户,避免一些命令可能出现干扰,或者说与其他项目隔离开,具体操作如下,比较简单,看图操作即可。


五、交叉编译环境配置

在路径 *“ linaro release**->compoments->toolchain->binaries->7.2-2017.11->arm-linux-gnueabi->gcc-linaro-7.2.1-2017.11-x86_64_arm-linux-gnueabi.tar.xz ”* 中下载对应的交叉编译器,记住是release版本,不是snapshots版本。(Snapshot版本代表不稳定、尚处于开发中的版本,快照版本。Release版本则代表稳定的版本,发行版本。)具体链接如下:

Linaro Releaseshttps://releases.linaro.org/components/toolchain/binaries/7.2-2017.11/arm-linux-gnueabi/下载完成,使用如下面命令解压缩:

1
tar -vxf gcc-linaro-7.2.1-2017.11-x86_64_arm-linux-gnueabi.tar.xz 

将解压的文件复制到/usr/local/arm/中,操作命令如下:

1
sudo cp -rf /home/project01/pro01/gcc-linaro-7.2.1-2017.11-x86_64_arm-linux-gnueabi ./

**然后添加该*交叉编译器的环境变量,*只有这样编译器才能在任何目录或者任何位置打开的终端中执行,打开~/.bashrc文件 *(修改.bashrc文件,只是针对某一个特定的用户;修改/etc/profile文件,它是针对于所有的用户)*,写入以下内容:

1
export PATH=$PATH:/usr/local/arm/gcc-linaro-7.2.1-2017.11-x86_64_arm-linux-gnueabi/bin

使环境变量生效:

1
source ~/.bashrc 

验证是否成功:

1
arm-linux-gnueabi-gcc -v

六、uboot简介

bootloader主要的工作就是启动 Linux 内核,bootloader和 Linux内核的关系就跟PC上的BIOS和 Windows的关系一样****,**bootloader就相当于BIOS。**常见的bootloader有 U-Boot、vivi、RedBoot 等等。

本文使用uboot作为系统的bootloader,它的全称是**”Universal Boot Loader”,意为“通用引导加载程序”。uboot是一种裸机程序,可以视为是一个汇集了多种裸机功能的综合示例。**目前的uboot已经支持液晶显示屏、网络连接、USB等高级功能。

uboot官方会维护uboot源码,也是最原汁原味的源码,但是官方提供的代码是供半导体厂商,也就是生产F1C200s的厂商使用的,他们会维护一个定制版本的uboot,但是,很遗憾,全志并没有将资料公开。**(注:后来发现,这里有全志的uboot,不知道这个是否是官方的网站,但已经好久没有更新)*由于网上关于F1C200s的uboot从零开始的开发资料几乎没有,此处我们使用荔枝派*的uboot启动Linux。(后面有时间再完善,欢迎大佬加群或评论区指导)**


七、uboot移植

🍏 uboot下载

**首先在github上*下载Lichee-Pi提供的uboot,*当然,前提是你的虚拟机能访问网络,如果不会配置,参看我左侧专栏虚拟机中的文章即可。或者直接在Windows中github上下载,然后传到ubuntu中解压。

uboot下载完成后,打开文件夹,uboot文件内容如下图所示:

🍏 uboot默认配置

对于同一款芯片,比如F1C200s,可能有不同的外设,uboot需要初始化的内容也不相同,因此需要选用uboot中的某些配置,去初始化各个外设,在uboot中的configs文件夹中存储着一些配置“套餐”,就是已经固定配置某些外设的**默认配置文件**,**一旦进行编译,uboot会根据这个配置文件进行配置。**

如上图所示,为uboot的configs目录下的两个荔枝派的配置文件,第一个licheepi_nono_defconfig 是针对**TF卡*的配置文件,第二个licheepi_nano_spiflash_defconfig*是针对flash的启动文件,显然我们需要选择第一个配置文件**编译uboot,因为我们只有TF卡一种存储介质,没有flash。下面是编译指令:

1
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- licheepi_nano_defconfig

为了避免使用类似于上方的繁琐编译指令,在uboot根目录Makfile中加入如下内容:

这样我们可以通过以下简洁的代码进行uboot配置:

1
2
3
4
5
# 进入u-boot目录
cd u-boot/

# 加载配置文件
make licheepi_nano_defconfig

经过以上操作,默认配置文件licheepi_nano_deconfig已经写入到,**/uboot/.config文件中,这是根据默认配置文件,生成的uboot的最终配置文件**,这个配置文件记录了所有配置选项的宏开关,我们可以通过宏开关对其进行修改。

注意:硬件中USB T口连接uart0,而荔枝派默认初始化uart0,因此无需修改。

🍏 uboot图形界面配置

使用make menuconfig命令进行图形界面配置uboot。

  1. bootcmd,主要用于描述控制Linux内核文件以及其他描述文件加载到内存中位置以及启动Linux内核系统等
  2. bootargs,用于配制文件系统、串口信息等。

Enable boot arguments 选项上点击空格,弹出Boot arguments选项,选中回车输入以下内容后回车保存。

1
console=ttyS0,115200 panic=5 rootwait root=/dev/mmcblk0p2 earlyprintk rw

同样的操作输入bootcmd的值,输入完成后如下图所示,有关bootargs和bootcmd值的含义,后序文章进行分析,这里不做解释。

1
load mmc 0:1 0x80008000 zImage;load mmc 0:1 0x80c08000 suniv-f1c100s-licheepi-nano.dtb;bootz 0x80008000 - 0x80c08000;

🍏 uboot编译

在进行编译之前需要在Ubuntu 中安装 ncurses 库,否则可能编译会报错,安装命令如下:

1
sudo apt-get install libncurses5-dev

使用make -j2进行编译,其中-j2代表处理器核心数。编译完成,在根目录下找到u-boot-sunxi-with-spl.bin文件,该文件为最终烧录文件。

🍏 烧录bin文件

只要将u-boot-sunxi-with-spl.bin烧录到tf卡的8k偏移地址处就可以了,至于为什么,上面的引用已经解释清楚了。将准备号的TF卡插入读卡器,使用如下块搬移命令进行烧写:

1
sudo dd if=u-boot-sunxi-with-spl.bin of=/dev/sdb bs=1024 seek=8

if 文件名:输入文件名,缺省为标准输入。即指定源文件。< if=input file >

of 文件名:输出文件名,缺省为标准输出。即指定目的文件。< of=output file >

bs bytes:同时设置读入/输出的块大小为bytes个字节。

seek blocks:从输出文件开头跳过blocks个块后再开始复制。

这里的输出文件(of)为主机电脑的/dev/sdb文件,也就是插入的读卡器代表的TF卡。

烧录完成,如下图所示,然后弹出读卡器之后再拔出,否则可能损坏读卡器。


八、uboot启动测试

将开发板使用数据线与电脑相连,打开串口调试工具,根据bootargs参数设置串口通信参数:

正常启动,窗串口调试工具为SecureCRT,选中端口后,波特率设置为115200:

墨云:

因为在你插入USB通电的时候开发板就已经启动了,所以当你打开串口连接的时候可能未必会看到信息,所以按一下重启键,就可以看到如下的输出信息了,这就是我们的u-boot,执行到u-boot计数完成后会产生错误,那是因为我们还没有进行系统内核的移植,所以默认就会进入u-boot命令模式。

点击复位按钮之后,在3秒之内(我设置的5秒)点击回车即可进入uboot命令模式,在 uboot命令模式输入“print”来查看环境变量 bootargs和bootcmd的值如下,和我们设置的相同,表示uboot启动成功!

至此,完成uboot移植全部内容,然而并没有像我想的那样,从零开始,按照官方的uboot一步一步自己移植,谁让我是个小白呢。


内核移植

一、bootloader、kernel、rootfs联系

kernel可以理解为一个庞大的裸机程序,和uboot以及其他比如点灯类似的裸机程序没有本质区别,只是kernel分为用户态和内核态,内存和设备操作与裸机程序不同。kernel是最底层,负责各种外设硬件的驱动,kernel类似于黑盒子,从外面只能看到接口,无法看到具体功能是如何实现的,内核初始化提供的接口后,将控制权通过init程序交给根文件系统。

详见:一文讲解Linux内核中根文件系统挂载流程 - 知乎


二、内核移植

1. 内核源码获取

下载Linux5.7.1源码 ,下载后完成后,将代码复制到Ubuntu新建的用户中并解压。

或者在国内镜像网站 下载,速度相比于官网快很多很多。

2. 内核配置与编译

🍍 基础配置与编译

解压完成后,使用VScode打开linux源码,同uboot编译前一样进行配置,首先在顶层Makfile中指定架构和交叉编译工具。*注意:arm必须是小写,必须!*

在uboot移植中,我们使用licheepi_nano_defconfig配置uboot,以识别开发板(不同开发板拥有不同的外设),同样,Linux内核也需要进行配置,在墨云对Linux内核进行移植时,提到:

进入内核源码中的arch/arm/configs目录中,可以看到有很多开发板的配置文件,其中sunxi_defconfig是全志的配置文件,但是该配置文件非常不全,需要额外配置大量的选项,一般选项多大上千个,这里先使用**licheepi_nano的配置文件。**

因此,同样,作为新手来讲,只能使用荔枝派的配置文件,下载文件后放到arch/arm/configs文件夹中,在根目录使用以下命令配置Linux内核:

1
make linux-licheepi_nano_defconfig

可能会报错:

使用如下命令安装组件:

1
2
sudo apt-get install flex
sudo apt-get install bison -y

如果出现以下错误,考虑更换镜像源,注意一定要按照自己的Ubuntu版本选择对应的源,最好更新为最新的清华源:

使用make命令编译,经过漫长的等待后,在 arch/arm/boot目录下生成内核文件:zImage,arch/arm/boot/dts目录下设备树文件:suniv-f1c100s-licheepi-nano.dtb。

如果出现以下错误,使用sudo apt install libssl-dev命令安装对应缺失库文件即可。

🍍 TF卡分区

uboot移植的时候**bootcmd*变量记录了内核文件(zImage)*和**设备树(.dtb文件)*的存放位置,那么现在我们就要准备好TF卡的分区,一旦编译出内核文件和设备树文件,就可以放到TF卡的指定位置*,启动Linux内核。

我们需要将TF卡分区如下,其中uboot只能识别FAT16格式,EXT4格式为Linux内核识别格式。注意:一定要把上面图片中的分区删除,否则无法识别到启动文件(zImage、.dtb)。

分区 分区一 分区二
名称 kernel rootfs
分区格式 FAT16 EXT4
大小 32M(可以随意填写) 剩余空间

TF卡配置完成如下图所示,注意我的TF卡是8G容量。

注意需要给uboot预留1M的空间,在【之前的空余空间】选择1M即可,在上面的可视化分区中无法看到这个预留空间。

🍍 内核烧录

将上面编译产生的zImage、suniv-f1c100s-licheepi-nano.dtb两个文件拷贝到KERNEL分区。将TF卡插好之后上电,打开串口调试,按下复位按键,等待uboot启动,5秒倒计时结束,读取两个文件,启动Linux内核。至此,Linux内核移植完成,下一步是Linux根文件系统(rootfs)。


根文件系统

一、根文件系统介绍

文件系统是对一个存储设备上的数据和元数据进行组织的机制,这种机制有利于用户和操作系统的交互。在Linux没有文件系统的话,用户和操作系统的交互也就断开了。

Unix没有盘符的概念,要求自己的文件系统是单一的一棵树。直接挂载在整棵树根上的那个盘里面的文件系统,就是根文件系统。

文件系统不是指某一个文件系统类型,而是指任何可以将文件目录组织成特定系统所需结构的文件系统,然后把它挂载在系统的上。

Linux中的根文件系统像是一个文件夹管理系统,或者说像是一本书的目录,在某种意义上讲,根文件系统就是一个文件夹,只不过是这是一个特殊的文件夹,在这个目录里面会有很多的子目录,如下图所示。

下面是虚拟机中Ubuntu系统的根文件系统,根文件系统的目录名字为 ‘/’ ,也就是一个斜杠,各个目录的功能在下面的表格中做了详细介绍。

Item Description
/ (root filesystem) 根文件系统是文件系统的最高级别目录。在其他文件系统被挂载之前,它必须包含启动Linux系统所需的所有文件。它必须包括启动其余文件系统所需的所有可执行文件和库。在系统启动后,所有其他文件系统都被挂载在标准的、定义明确的挂载点上,作为根文件系统的子目录。
/bin /bin目录包含用户可执行文件。
/boot 包含启动Linux计算机所需的静态引导程序和内核可执行文件以及配置文件。
/dev 这个目录包含了连接到系统的每个硬件设备的设备文件。这些不是设备驱动程序,而是代表计算机上每个设备的文件,并为访问这些设备提供便利。
/etc 包含主机的本地系统配置文件。
/home 用户文件的主目录存储。每个用户都有一个/home的子目录。
/lib 包含启动系统所需的共享库文件。
/media 一个安装外部可移动媒体设备的地方,如可能连接到主机的USB拇指驱动器。
/mnt 普通文件系统(如不是可移动媒体)的临时挂载点,可以在管理员修复或处理文件系统的时候使用。
/opt 可选的文件,如供应商提供的应用程序应位于这里。
/root 这不是根(/)文件系统。它是根用户的主目录。
/sbin 系统二进制文件。这些是用于系统管理的可执行文件。
/tmp 临时目录。由操作系统和许多程序用来存储临时文件。用户也可以在这里临时存储文件。请注意,存储在这里的文件可能在任何时候被删除,而无需事先通知。
/usr 这些是可共享的、只读的文件,包括可执行的二进制文件和库、人文件和其他类型的文件。
/var 可变的数据文件被存储在这里。这可以包括像日志文件、MySQL和其他数据库文件、网络服务器数据文件、电子邮件收件箱,以及更多的东西。

二、根文件系统移植

1、buildroot下载

我们使用buildroot制作根文件系统,之前imx6ull使用的busybox,这里换一下,哈哈,首先进入官网,下载根文件系统buildroot2018.2.11版本,与墨云保持一致,下载后放到Ubuntu中解压。https://buildroot.org/download.htmlimghttps://buildroot.org/download.html

这个网站比较友好,不需要魔法,buildroot体积较小,直接下载即可,下面是几个不同后缀的文件。

上面的四个选项,下载后是下面的文件,sign是什么文件我不清楚,嘿嘿,欢迎评论区指出,选择一个压缩包,到Ubuntu中解压

2、根文件系统制作

解压完成后进入根目录,清理工程后,进入图形界面配置:

1
2
make clean
make menuconfig

图形配置界面如下图所示,使用方向键选择不同选项,空格进行选中,回车进行确认。

Target options选项的配置如下图所示,每个选项的含义墨云解释过了,我就不造轮子了。

  • 第一个选项为架构选择,这里选择ARM架构小端模式,
  • 第二个为输出的二进制文件格式,这里选择EFL格式,
  • 第三个为架构体系,这里选择arm926t,因为F1C200S/F1C100S的架构就是这个架构,
  • 第四个为矢量浮点处理器,这里不勾选,因为对于F1C200S/F1C100S而言,其内部没有浮点运算单元,只能进行软浮点运算,也就是模拟浮点预运算。
  • 第五个为应用程序二进制接口,这里选择EABI,原因是该格式支持软件浮点和硬件实现浮点功能混用。
  • 第六个为浮点运算规则,这里使用软件浮点
  • 第七个选择指令集,这里选择ARM指令集,因为thumb主要针对Cortex M系列而言的,对于运行操作系统的A系列以及ARM9和ARM11而言,使用的都是32位的ARM指令集。

来自:小白自制Linux开发板 三. Linux内核与文件系统移植 - 淡墨青云 - 博客园

据说Toolchain按照下方配置(打开了一些功能)可以在开发板上直接编译程序。

登录的时候会显示 “Wecome to kashine linux system.” ,并且我们设置了root用户密码为“good luck!”。

配置完成后,使用make命令进行编译,从下面可以看出,buildroot的编译需要网络支持,以通过网络配置我们选择的内容。 当然如果你的虚拟机无法连接网络,请看虚拟机ubuntu桥接怎么联网,或者是虚拟机net模式访问互联网

此处是漫长的等待……….,建议来一道力扣中等!复杂链表的复制!力扣https://leetcode.cn/problems/fu-za-lian-biao-de-fu-zhi-lcof/?envType=study-plan&id=lcof&plan=lcof&plan_progress=y16046r

这个下载速度真的是奇慢无比!如果下载依赖包或者软件速度非常慢,可以尝试(不要试了,我试过不管用,哈哈)Ubuntu20.04换源之后依旧慢?Ubuntu16.04找不到Software&Updates(软件更新),但我试了一下仍然很慢,这不科学呀!魔法也不管用。

终于编译完成了!已经是第二天了,昨天搞到1:00还没有编译完,我就直接挂起虚拟机,今天早晨打开Ubuntu没大会,就已经编译完成了,大概两个多小时。编译完成后,在buildroot根目录的****output/images目录*下生成一个rootfs.rar文件*,这个就是我们心心念念的根文件系统。

注意:第二次编译就会快很多,比如误删,别问我怎么知道的,唔哈哈哈。

3、根文件系统移植

将上面得到的rootfs.rar解压到TF卡的第二个分区,也就是rootfs分区,不要解压完成在复制过去,因为解压出来好多文件夹。两种选择,要么把压缩包复制到rootfs分区,解压后删除压缩包,要么直接解压到第二分区。使用如下命令将压缩包解压到rootfs分区后如下图所示:

1
sudo tar -vxf rootfs.tar -C /media/project01/rootfs/

其中tar解压缩命令格式如下:

选项 含义
-x 对 tar 包做解打包操作。
-f 指定要解压的 tar 包的包名。
-t 只查看 tar 包中有哪些文件或目录,不对 tar 包做解打包操作。
-C 目录 指定解打包位置。
-v 显示解打包的具体过程。

4、根文件系统加载

TF卡第二个分区内已经放置了我们制作好的根文件系统,将TF卡插到开发板上并上电启动,打开串口调试助手,设置波特率115200,可以看到下面的内容,uboot和内核启动成功。

但是并没有启动Linux的根文件系统,我们看最后打印出的提示代码为: Waiting for root device /dev/mmcblk0p2…,看来是mmcblk0p2表示mmc设备(MultiMedia Card,多媒体存储卡)没有被内核启动成功,mmcblk0是我们的TF卡,p2代表第二个分区,我们最终确定是mmc设备出了问题。

5、mmc设备问题分析

我们先来分析一下出现这个问题的原因,如果说mmcblk0p2设备出现问题,那么为什么在启动Linux内核的时候没有出错的,内核文件和设备树文件存放在mmcblk0p1中,如果是mmcblk0p2加载出错,也就表示TF卡的设备树或者是驱动出了问题,那么mmcblk0p1不应该也是错的吗?那Linux内核不应该能够启动才对的?

小朋友,你是否有很多问号?好的,现在来说一下我对上面问题的理解。其实上面的问题有点不太对,不知道大家是否有相同的疑问,所以我对上面也进行了保留。下面分析:首先是uboot的启动,在TF卡的8k处,我们硬件没有问题,也就是说TF卡接在F1C200s的指定引脚,bootROM启动的时候一定会读取TF卡的8k位置,成功启动uboot,而在uboot启动后,进入倒计时,倒计时结束后,****执行bootcmd命令,bootcmd命令如下所示:

1
load mmc 0:1 0x80008000 zImage;load mmc 0:1 0x80c08000 suniv-f1c100s-licheepi-nano.dtb;bootz 0x80008000 - 0x80c08000;

该命令的主要作用是将zImage和设备树从mmc的第一个分区拷贝到内存中执行,这个应该好理解哈,Linux内核启动成功了,说明uboot能够将两个文件拷贝到内存中执行,进一步说明**uboot的TF卡驱动**是没有问题的,**拷贝结束后,uboot生命周期结束,Linux内核启动,但是Linux内核TF卡启动出错**,导致无法加载TF卡第二分区中的根文件系统。

至此,我们大致分析出导致无法启动Linux根文件系统的原因是:TF卡设备树或者驱动出错。对于设备树来讲,我们只需要提供对应的硬件信息,即可在不同的开发板可以使用相同的驱动。形象一点讲就是:对某个LED点灯程序(驱动),我宏定义了一个LED管脚(设备树),程序是官方给的,也就是说确定这个程序可以点灯,现在我们将这套程序放到另一个相同主控的开发板上运行,出问题的话就很可能是我们的宏定义出错了,也就是说,很可能是设备树出错。

通过上面的分析,我们怀疑很可能是设备树出了问题,那下面我们对设备树进行检查。首先介绍一下设备树相关文件,打开**/arch/arm/boot/dts可以看到我们我们前面拷贝到TF卡KERNEL分区的设备树文件,其中.dtsi文件为“头文件”,储存一个主控芯片的共同信息,供针对相同主控芯片不同开发板的设备树源文件.dts文件调用,编译后生成设备树文件.dtb。**

打开**/arch/arm/boot/dts目录下的suniv-f1c100s-licheepi-nano.dts、suniv-f1c100s.dtsi,在dtsi文件中并没有发现mmc的内容,也就是如orange2c所说,是因为我们使用的主线Linux内核源码,而主线Linux设备树中并没有开启mmc。**

6、mmc功能开启

参考荔枝派设备树源码,进行以下修改。首先添加头文件:

1
2
#include <dt-bindings/clock/suniv-ccu-f1c100s.h>
#include <dt-bindings/reset/suniv-ccu-f1c100s.h>

然后在soc结点下的pio添加如下代码,其中PF0、PF1、PF2、PF3、PF4、PF5为TF卡相关引脚,详见https://blog.csdn.net/qq_41709234/article/details/124389957。

1
2
3
4
mmc0_pins: mmc0-pins {
pins = "PF0", "PF1", "PF2", "PF3", "PF4", "PF5";
function = "mmc0";
};

在soc结点下添加如下代码,使用pinctrl子系统初始化引脚,其中compatible变量保存着mmc对应的驱动。其他时钟总线之类的我就不详细解释了,因为我也不懂,哈哈。但是一定要注意,.dtsi文件里面是默认关闭mm功能的(可以看到下面的status是disabled状态),需要在.dts里面使能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mmc0: mmc@1c0f000 {
compatible = "allwinner,suniv-f1c100s-mmc",
"allwinner,sun7i-a20-mmc";
reg = <0x01c0f000 0x1000>;
clocks = <&ccu CLK_BUS_MMC0>,
<&ccu CLK_MMC0>,
<&ccu CLK_MMC0_OUTPUT>,
<&ccu CLK_MMC0_SAMPLE>;
clock-names = "ahb",
"mmc",
"output",
"sample";
resets = <&ccu RST_BUS_MMC0>;
reset-names = "ahb";
interrupts = <23>;
pinctrl-names = "default";
pinctrl-0 = <&mmc0_pins>;
status = "disabled";
#address-cells = <1>;
#size-cells = <0>;
};

.dtsi文件修改完成后如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// SPDX-License-Identifier: (GPL-2.0+ OR X11)
/*
* Copyright 2018 Icenowy Zheng <icenowy@aosc.io>
* Copyright 2018 Mesih Kilinc <mesihkilinc@gmail.com>
*/

// modify by kashine
#include <dt-bindings/clock/suniv-ccu-f1c100s.h>
#include <dt-bindings/reset/suniv-ccu-f1c100s.h>

/ {
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;

clocks {
osc24M: clk-24M {
#clock-cells = <0>;
compatible = "fixed-clock";
clock-frequency = <24000000>;
clock-output-names = "osc24M";
};

osc32k: clk-32k {
#clock-cells = <0>;
compatible = "fixed-clock";
clock-frequency = <32768>;
clock-output-names = "osc32k";
};
};

cpus {
cpu {
compatible = "arm,arm926ej-s";
device_type = "cpu";
};
};

soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;

sram-controller@1c00000 {
compatible = "allwinner,suniv-f1c100s-system-control",
"allwinner,sun4i-a10-system-control";
reg = <0x01c00000 0x30>;
#address-cells = <1>;
#size-cells = <1>;
ranges;

sram_d: sram@10000 {
compatible = "mmio-sram";
reg = <0x00010000 0x1000>;
#address-cells = <1>;
#size-cells = <1>;
ranges = <0 0x00010000 0x1000>;

otg_sram: sram-section@0 {
compatible = "allwinner,suniv-f1c100s-sram-d",
"allwinner,sun4i-a10-sram-d";
reg = <0x0000 0x1000>;
status = "disabled";
};
};
};

ccu: clock@1c20000 {
compatible = "allwinner,suniv-f1c100s-ccu";
reg = <0x01c20000 0x400>;
clocks = <&osc24M>, <&osc32k>;
clock-names = "hosc", "losc";
#clock-cells = <1>;
#reset-cells = <1>;
};

intc: interrupt-controller@1c20400 {
compatible = "allwinner,suniv-f1c100s-ic";
reg = <0x01c20400 0x400>;
interrupt-controller;
#interrupt-cells = <1>;
};

pio: pinctrl@1c20800 {
compatible = "allwinner,suniv-f1c100s-pinctrl";
reg = <0x01c20800 0x400>;
interrupts = <38>, <39>, <40>;
clocks = <&ccu 37>, <&osc24M>, <&osc32k>;
clock-names = "apb", "hosc", "losc";
gpio-controller;
interrupt-controller;
#interrupt-cells = <3>;
#gpio-cells = <3>;

uart0_pe_pins: uart0-pe-pins {
pins = "PE0", "PE1";
function = "uart0";
};

// modify by kashine
mmc0_pins: mmc0-pins {
pins = "PF0", "PF1", "PF2", "PF3", "PF4", "PF5";
function = "mmc0";
};
};

timer@1c20c00 {
compatible = "allwinner,suniv-f1c100s-timer";
reg = <0x01c20c00 0x90>;
interrupts = <13>;
clocks = <&osc24M>;
};

wdt: watchdog@1c20ca0 {
compatible = "allwinner,suniv-f1c100s-wdt",
"allwinner,sun4i-a10-wdt";
reg = <0x01c20ca0 0x20>;
};

uart0: serial@1c25000 {
compatible = "snps,dw-apb-uart";
reg = <0x01c25000 0x400>;
interrupts = <1>;
reg-shift = <2>;
reg-io-width = <4>;
clocks = <&ccu 38>;
resets = <&ccu 24>;
status = "disabled";
};

uart1: serial@1c25400 {
compatible = "snps,dw-apb-uart";
reg = <0x01c25400 0x400>;
interrupts = <2>;
reg-shift = <2>;
reg-io-width = <4>;
clocks = <&ccu 39>;
resets = <&ccu 25>;
status = "disabled";
};

uart2: serial@1c25800 {
compatible = "snps,dw-apb-uart";
reg = <0x01c25800 0x400>;
interrupts = <3>;
reg-shift = <2>;
reg-io-width = <4>;
clocks = <&ccu 40>;
resets = <&ccu 26>;
status = "disabled";
};

// modify by kashine
mmc0: mmc@1c0f000 {
compatible = "allwinner,suniv-f1c100s-mmc",
"allwinner,sun7i-a20-mmc";
reg = <0x01c0f000 0x1000>;
clocks = <&ccu CLK_BUS_MMC0>,
<&ccu CLK_MMC0>,
<&ccu CLK_MMC0_OUTPUT>,
<&ccu CLK_MMC0_SAMPLE>;
clock-names = "ahb",
"mmc",
"output",
"sample";
resets = <&ccu RST_BUS_MMC0>;
reset-names = "ahb";
interrupts = <23>;
pinctrl-names = "default";
pinctrl-0 = <&mmc0_pins>;
status = "disabled";
#address-cells = <1>;
#size-cells = <0>;
};
};
};

在**.dts文件里面进行以下修改,首先使能mmc:**

1
2
3
4
5
6
&mmc0 {
vmmc-supply = <&reg_vcc3v3>;
bus-width = <4>;
broken-cd;
status = "okay";
};

然后这个是墨云****在根节点下添加的一个结点,他并没有说明为什么添加,我百度了一下,这是一个电源管理相关的结点,使用regulator-fixed来实现使用GPIO控制某个电源开关 ,希望在开机时尽快输出高低电平来控制电源。详见。Rockchip RK3588 kernel dts解析之regulator-fixed

1
2
3
4
5
6
reg_vcc3v3: vcc3v3 {
compatible = "regulator-fixed";
regulator-name = "vcc3v3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
};

.dts文件修改完成之后下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// SPDX-License-Identifier: (GPL-2.0+ OR X11)
/*
* Copyright 2018 Icenowy Zheng <icenowy@aosc.io>
*/

/dts-v1/;
#include "suniv-f1c100s.dtsi"

/ {
model = "Lichee Pi Nano";
compatible = "licheepi,licheepi-nano", "allwinner,suniv-f1c100s";

aliases {
serial0 = &uart0;
};

chosen {
stdout-path = "serial0:115200n8";
};

// modify by kashine
reg_vcc3v3: vcc3v3 {
compatible = "regulator-fixed";
regulator-name = "vcc3v3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
};
};

&uart0 {
pinctrl-names = "default";
pinctrl-0 = <&uart0_pe_pins>;
status = "okay";
};

// modify by kashine
&mmc0 {
vmmc-supply = <&reg_vcc3v3>;
bus-width = <4>;
broken-cd;
status = "okay";
};

7、设备树编译与下载

在Linux内核根目录下,使用以下命令重新编译设备树,内核无需重新编译。

1
make dtbs

在内核源码根目录使用make dtbs编译变成后生成设备树文件,如下图所示。

编译完成后,将新的设备树文件(.dtb)拷贝到TF卡的第二分区,**zImage无需修改,*打开串口调试助手,上电复位,成功启动根文件系统!!!*,成功打印出“Welcome to kashine linux system.”,root为管理员用户名,密码为前面设置的“good luck!”,**将路径切换到根目录之后,查看根目录的文件夹,如下图所示。

在上电成功启动根文件系统之后,不要直接拔掉USB断电,可能造成根文件系统损坏,使用poweroff命令****关闭根文件系统后断电。

Debian系统

一、Debian系统介绍

Debian GNU/Linux 是 Linux 操作系统的一个特定发行版,以及在其上运行的众多软件包。Debian项目由 Ian Murdock 于 1993 年发起,是在Linux发行版的概念还很新的时候获得广泛认可的首批自由软件项目之一。Debian 这个名字——发音为 deb-EE-uhn——是一个合成词,它混合了创建者的名字 Ian 和他妻子 Debra 的名字。


二、Linux发行版和Linux内核的关系

**内核:**位于应用软件和底层硬件系统之间。它直接与硬件通信,传递应用软件的任何请求。****一个简单的例子来说明这一点,就是在手机上录制视频。当你点击相机应用程序时,该软件将向内核传达它想要使用相机和麦克风的信息。然后,内核将通知摄像头和麦克风硬件唤醒并做好准备。那么现在,软件和硬件将协同工作,为你录制好视频。

**操作系统:**主要目的是管理系统进程和资源。它包含内核,因此执行内核可以执行的所有任务。****此外,它还确保系统保护和安全。操作系统充当用户和底层硬件系统之间的接口。所有应用程序都在操作系统创建的封闭环境中运行。因此,没有操作系统就无法使用系统。

Linux 本身就是一个内核。然后,开发人员在其基础上进行构建,以提供当今可用的各种 Linux 发行版。


三、Debian文件系统制作

由于F1C200s的RAM只有64M,无法支持像是Ubuntu-Core这样的文件系统(最低RAM需求512M),所以一般只能用buildroot来生成简单的文件系统,或者裸机开发。

但是为了方便地使用Debian系的丰富软件,我们可以自己构建Debian最小系统,最小rootfs在180MB左右。

– 稚辉君

1、环境配置

首先下载两个工具:qemu-arm-static和debootstrap。

🥝 qemu-arm-static:通过qemu-arm-static,我们在x86的Ubuntu PC机上,可以模拟ARM处理器,就像运行在ARM上一样进行各种操作。这样既实现了ARM环境,又利用了x86 PC的处理能力。

🥝 debootstrap:是Debian/Ubuntu下的一个工具,用来构建一套基本的系统(根文件系统)。生成的目录符合Linux文件系统标准(FHS),即包含了/boot、/etc、/bin、/usr等等目录,但它比发行版本的Linux体积小很多,当然功能也没那么强大,因此,只能说是“基本的系统”。

1
2
3
sudo apt install qemu-user-static -y
sudo apt install debootstrap -y
mkdir Debian_rootfs

2、Debian文件系统构建

由于我们的Ubuntu是Linux系统,而开发板是ARM系统,在使用bootstrap为外部体系结构引导发行版时,才需要分开两个阶段。请参见手册页–foreign上的选项说明。

例如,使用x64机器为嵌入式ARM系统创建Debian安装系统。第一阶段将下载所需的.deb文件并将其解压缩到您指定的目录中。第二阶段解压运行所有程序包配置脚本,这些脚本必须使用目标体系结构或使用qemu-user-static模拟目标体系结构来完成,在此处我们使用qemu-user-static模拟ARM系统来完成。在完成两个阶段的配置后,还需要进入qemu模拟器进行一些必要配置。

🍟 第一阶段

使用华为镜像源抽取根文件系统**(下载deb软件包)**,**命令如下,bootstrap命令详解见Bootstrap命令详解。其中foreign表示若目标架构与本机架构不符时,需要携带该参数;arch代表架构,armhf (支持硬件浮点)、armel (软浮点);verbose表示不打印wget等包下载数据,进行静默安装;buster**代表Debian发行版本代号。

1
sudo debootstrap --foreign --verbose --arch=armel  buster rootfs http://mirrors.huaweicloud.com/debian/

其中,不同Debian发行版代号如下。命令执行完成后就是大家最常见的linux根文件系统,但是目前还不能直接使用,还需要对其做一些其他的配置。

版本号 代号 备注
Debian 11 bullseye 稳定版
Debian 10 buster 旧稳定版
Debian 9 stretch 旧稳定版
Debian 8 jessie 已存档版
Debian 7 wheezy 被淘汰版
Debian 6 squeeze 被淘汰版
Debian 5 lenny 被淘汰版

至此,已经下载了最小的Debian系统, 你也可以将它想象为”最小系统”类似的存在,没有其他 “外设” 。下图是我补充的图片,所在文件夹名字和上面不同。

🍟 第二阶段

上面讲在第二阶段我们应该使用qemu-user-static模拟ARM系统运行所有程序包配置脚本,怎么模拟ARM系统呢?使用qemu这个虚拟机。简而言之,我们需要在qemu模拟出的ARM系统中执行一个Debian系统配置,相当于把Debian放到ARM开发板上配置。具体操作为:文件夹挂载 + chroot切换根目录。

1)文件挂载:

首先对文件夹按照下面的命令进行挂载,这里为什么需要挂载呢?

1
2
3
4
5
cd rootfs
sudo mount --bind /dev dev/
sudo mount --bind /sys sys/
sudo mount --bind /proc proc/
sudo mount --bind /dev/pts dev/pts/

我的理解是:因为在宿主(Ubuntu虚拟机)是x64架构,不能安装arm架构的软件,需要依靠qemu-arm-static模拟成arm环境进行安装,而模拟arm环境相当于,以指定的某个目录为新的根目录,如果不更改挂载点,直接下载一些软件或者依赖项就会下载到原来的挂载点而出错。

通俗一点的理解:虽然更新了根目录,如果没有挂载新文件系统的/dev等文件夹,如果再去操作/dev,相当于还是在操作Ubuntu的/dev,只有挂载了这个文件夹之后,再操作才相当于真正操作Debian新的根文件系统的/dev。评论区讨论吧!欢迎指正!

2)chroot切换根目录:

chroot,即 change root directory (更改 root 目录)。在 linux 系统中,系统默认的目录结构都是以 /,即以根 (root) 开始的。而在使用 chroot 之后,系统的目录结构将以指定的位置作为 / 位置。

1
sudo cp /usr/bin/qemu-arm-static  usr/bin/

使用上面的命令将我们之前安装的qemu复制到/rootfs/usr/bin目录下,可以看到我们具有该文件的读、写、可执行权限。

然后在Debian_rootfs目录下执行下面的命令,该命令意思是设置一些环境变量,并使用chroot命令将rootfs切换为新的根目录,执行目录rootfs/debootstrap下的命令: debootstrap –second-stage,后面的是debootstrap可执行文件的两个参数。

1
sudo LC_ALL=C LANGUAGE=C LANG=C chroot rootfs /debootstrap/debootstrap --second-stage --verbose

该命令的主要作用是:对拉取的Debian根文件系统进行配置。这主要是由上面的–foreign选项决定的。这里是使用qemu-user-static模拟目标体系结构来完成的,因为我们使用的chroot更改了根目录,并挂载了新的文件系统的文件夹,相当于使用chroot命令+挂载,完成arm架构的模拟。


三、Debian文件系统配置

1
sudo LC_ALL=C LANGUAGE=C LANG=C chroot rootfs

使用上面的命令进入qemu虚拟器,相当于进入ARM系统下的Debian文件系统中,如下图所示。chroot rootfs 后面不加命令相当于执行chroot rootfs /bin/bash,也就是打开一个shell,详见chroot语法。

*1、更新源*

更新源为国内源可以提高后续软件下载速度,具体命令如下。因为默认的源是 http 的,但是如果准备使用的镜像源是https 的,所以需要额外的安装有关 HTTPS 的包。所以安装这两个包之后就可以正常拉取https的源了:apt-transport-https和ca-certificates。

1
2
3
vi /etc/apt/sources.list
#写入: deb http://mirrors.huaweicloud.com/debian buster main
apt-get update

*2、安装网络相关库*

1
2
3
4
5
6
7
8
9
10
11
12
apt-get install wpasupplicant #安装WIFI配置相关的组件
apt-get install net-tools #安装网络基础组件、如使用ifconfig等
apt-get install udhcpc #当wifi连接成功后,需要用这个组件去获取IP地址

apt-get install evtest #触摸屏测试
apt-get install mplayer
apt-get install alsa-utils #音频测试

## 其他组件
apt-get install wireless-tools
apt install sudo vim openssh-server htop
apt install pciutils usbutils acpi #acpi我没有安装成功,换了其他的源也不可以

*3、配置账号*

设置root账号密码。

1
passwd root

*4、配置时区*

1
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

*5、配置SSH*

1
2
vi /etc/ssh/sshd_config
#写入: PermitRootLogin yes

*6、rootfs打包*

1
2
3
4
5
6
7
8
9
apt clean #清理
exit #退出chroot
rm rootfs/usr/bin/qemu-arm-static #删除之前拷贝的文件
cd rootfs
sudo umount dev/pts/ # 一定要在/dev前面umount
sudo umount dev/
sudo umount sys/
sudo umount proc/
sudo tar cvf ../rootfs.tar ./ #在rootfs目录下执行

最终生成了Debian文件系统压缩包,解压即可使用。

*7、Debian文件系统启动*

rootfs.tar文件目录终端中,输入以下命令解压到TF卡的rootfs分区,也就是第二分区。

1
sudo tar -vxf ./rootfs.tar -C /media/project01/rootfs/

注意:一定要等待拷贝完成,再拔出TF卡,我的转接器上有指示灯,等待指示灯常亮,不再闪烁即可拔出。将TF卡插到开发板上,上电启动复位,等待,启动成功!!!


四、增加swap分区

芯片的SiP内存只有64MB,大部分情况下都不够用,所以需要开启swap使用内存卡的一部分空间来作为交换内存。

在使用一些软件的过程中,会遇到系统崩溃,尤其是使用 apt-get install 的时候,所以需要加入swap分区,可以简单理解为虚拟内存。

这十分类似于Windows的虚拟内存,虚拟内存其实就是将硬盘的一部分容量当内存使用,从而弥补物理内存不足的问题。

我们可以通过free -m来查看下内存使用状况,此处我直接在qemu虚拟器中查看:

使用如下命令创建你想要添加swap分区的大小:

1
dd if=/dev/zero of=/swap1 bs=1M count=512  #count是SWAP大小,512就是512MB

设置swap分区文件,然后激活分区:

1
2
3
4
5
6
7
# 设置分区文件
mkswap /swap1
# 激活swap分区
swapon /swap1
vi /etc/fstab
# 最后一行添加
# /swap1 swap swap defaults 0 0

将新的根文件系统拷贝到TF卡的第二分区,重新上电启动,查看交换分区大小,的确为我们设置的512M。

正点原子4.3寸RGB屏幕驱动

一、RGB屏幕介绍

正点原子ATK4384显示屏,分辨率800*480,支持RGB接口,详细资料下载地址为:4.3寸RGBLCD电容触摸屏800*480。RGB LCD接口信号线如下所示:

信号线 描述
R[7:0] 数据线
G[7:0] 数据线
B[7:0] 数据线
DE 数据使能线
VSYNC 垂直同步信号线
HSYNC 水平同步信号线
PCLK 像素钟信号线

屏幕的参数如下所示,其中各参数的含义详见这篇博客,解释的比较详细,我就不再重复造轮子了。

参数 单位
水平显示区域 800 每时钟 (tCLK)
HSPW (thp) 48 每时钟 (tCLK)
HBP (thb) 88 每时钟 (tCLK)
HFP (thf) 40 每时钟 (tCLK)
垂直显示区域 480 每行 (th)
VSPW (tvp) 3个 每行 (th)
VBP (tvb) 32 每行 (th)
VFP (tvf) 13 每行 (th)

我的板子上存在两个屏幕接口, 1.14寸ips屏幕、ATK4384的40pinrgb屏幕,由于1.14寸屏幕实在是太小了,因此,本系统使用的40PinRGB屏幕接口如图所示。由下面的硬件原理图可以看出我们采用的屏**数据格式是RGB666**,**当然,I2C接口用作触摸屏数据传输**,本文暂时用不到。


二、RGB屏幕驱动

linux内核已经配备了RGB模式下的LCD驱动(见drivers/gpu/drm/panel/panel-simple.c,也就是DRM驱动),而我们的F1C200s支持RGB模式的LCD,我们不需要从零写一个屏幕驱动,我们只需要简单几步即可驱动LCD屏幕。F1C200s虽然不像IMX6ULL那样配备eLCDIF接口,但只需要符合RGB接口的时序即可点亮。

注意:内核配置文件和之前相同!

1、设备树修改

.dtsi文件中做以下修改,代码修改处添加了modify by kashine的标注。其中,加入reset和clock两个文件夹下的suniv-ccu-f1c100s.h文件,是为了提供一些宏定义设备树de结点是display engine驱动初始化结点,官方解释为:Allwinner A10 Display Engine DRM/KMS Driver,关于DRM驱动的知识,这里不在赘述;tcon0结点对应时序控制驱动,官方解释为:Allwinner A10 Timing Controller Driver;pio结点中加入RGB屏幕的管脚信息;fe0结点和be0结点为RGB显示的前后端驱动程序;pwm结点用于背光显示,亮度调节。

我们来详细分析一下代码中的各个部分。

  1. display-engine:描述了显示引擎,这是 F1C100s 处理器中的一个重要组件,用于处理图像数据。它与显示前端(display-frontend)和显示后端(display-backend),相连接包含一条pipeline fe0。
  2. tcon0:代表了时序控制器(Timing Controller,简称 TCON),它负责生成正确的时序信号,以驱动显示面板。这部分代码中,我们可以看到 TCON 的寄存器地址、中断号、时钟和复位信号等信息。此外,还定义了输入和输出端口,分别连接到显示后端和显示面板。
  3. lcd_rgb666_pins:定义了与 RGB 屏幕连接的引脚,这里使用了 18 位的 RGB666 接口。
  4. display-frontend 和 display-backend:显示前端和显示后端分别负责图像数据的预处理和后处理。显示前端接收原始图像数据,对其进行缩放、色彩空间转换等操作;显示后端则负责图像数据的合成、alpha 混合等功能。这两个部分通过端口和端点(endpoint)进行连接。
  5. panel:描述了显示面板,定义了使用的LCD panel参数。这里使用了一个兼容于 “alientek,alientek_4p3_inch” 和 “simple-panel” 的面板。面板的输入端口与 TCON 的输出端口相连接。
  6. reg_vcc3v3:描述了一个固定电压的电源,为面板提供 3.3V 电源。

最后,在设备树末尾,启用了 be0(显示后端)、de(显示引擎)和 tcon0(时序控制器)节点。

从这段代码中可以看出,该 RGB 屏幕驱动确实使用了 DRM 框架。其工作原理大致如下:

  1. 显示前端接收原始图像数据,进行预处理。
  2. 处理后的图像数据传递给显示后端,进行后处理和合成。
  3. 处理后的图像数据通过 TCON 生成正确的时序信号,驱动显示面板。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
// SPDX-License-Identifier: (GPL-2.0+ OR X11)
/*
* Copyright 2018 Icenowy Zheng <icenowy@aosc.io>
* Copyright 2018 Mesih Kilinc <mesihkilinc@gmail.com>
*/

// modify by kashine
#include <dt-bindings/clock/suniv-ccu-f1c100s.h>// 下面引用该文件中的一些宏定义
#include <dt-bindings/reset/suniv-ccu-f1c100s.h>

/ {
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;

clocks {
osc24M: clk-24M {
#clock-cells = <0>;
compatible = "fixed-clock";
clock-frequency = <24000000>;
clock-output-names = "osc24M";
};

osc32k: clk-32k {
#clock-cells = <0>;
compatible = "fixed-clock";
clock-frequency = <32768>;
clock-output-names = "osc32k";
};
};

cpus {
cpu {
compatible = "arm,arm926ej-s";
device_type = "cpu";
};
};

// modify by kashine
de: display-engine {
compatible = "allwinner,suniv-f1c100s-display-engine";
allwinner,pipelines = <&fe0>;
status = "disabled";
};

soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;

sram-controller@1c00000 {
compatible = "allwinner,suniv-f1c100s-system-control",
"allwinner,sun4i-a10-system-control";
reg = <0x01c00000 0x30>;
#address-cells = <1>;
#size-cells = <1>;
ranges;

sram_d: sram@10000 {
compatible = "mmio-sram";
reg = <0x00010000 0x1000>;
#address-cells = <1>;
#size-cells = <1>;
ranges = <0 0x00010000 0x1000>;

otg_sram: sram-section@0 {
compatible = "allwinner,suniv-f1c100s-sram-d",
"allwinner,sun4i-a10-sram-d";
reg = <0x0000 0x1000>;
status = "disabled";
};
};
};

// modify by kashine
tcon0: lcd-controller@1c0c000 {
compatible = "allwinner,suniv-f1c100s-tcon";
reg = <0x01c0c000 0x1000>;
interrupts = <29>;
clocks = <&ccu CLK_BUS_LCD>,
<&ccu CLK_TCON>;
clock-names = "ahb",
"tcon-ch0";
clock-output-names = "tcon-pixel-clock";
resets = <&ccu RST_BUS_LCD>;
reset-names = "lcd";
status = "disabled";

ports {
#address-cells = <1>;
#size-cells = <0>;

tcon0_in: port@0 {
#address-cells = <1>;
#size-cells = <0>;
reg = <0>;

tcon0_in_be0: endpoint@0 {
reg = <0>;
remote-endpoint = <&be0_out_tcon0>;
};
};

tcon0_out: port@1 {
#address-cells = <1>;
#size-cells = <0>;
reg = <1>;
};
};
};

ccu: clock@1c20000 {
compatible = "allwinner,suniv-f1c100s-ccu";
reg = <0x01c20000 0x400>;
clocks = <&osc24M>, <&osc32k>;
clock-names = "hosc", "losc";
#clock-cells = <1>;
#reset-cells = <1>;
};

intc: interrupt-controller@1c20400 {
compatible = "allwinner,suniv-f1c100s-ic";
reg = <0x01c20400 0x400>;
interrupt-controller;
#interrupt-cells = <1>;
};

pio: pinctrl@1c20800 {
compatible = "allwinner,suniv-f1c100s-pinctrl";
reg = <0x01c20800 0x400>;
interrupts = <38>, <39>, <40>;
clocks = <&ccu 37>, <&osc24M>, <&osc32k>;
clock-names = "apb", "hosc", "losc";
gpio-controller;
interrupt-controller;
#interrupt-cells = <3>;
#gpio-cells = <3>;

uart0_pe_pins: uart0-pe-pins {
pins = "PE0", "PE1";
function = "uart0";
};

// modify by kashine
lcd_rgb666_pins: lcd-rgb666-pins {
pins = "PD0", "PD1", "PD2", "PD3", "PD4",
"PD5", "PD6", "PD7", "PD8", "PD9",
"PD10", "PD11", "PD12", "PD13", "PD14",
"PD15", "PD16", "PD17", "PD18", "PD19",
"PD20", "PD21";
function = "lcd";
};

// modify by kashine
mmc0_pins: mmc0-pins {
pins = "PF0", "PF1", "PF2", "PF3", "PF4", "PF5";
function = "mmc0";
};
};

timer@1c20c00 {
compatible = "allwinner,suniv-f1c100s-timer";
reg = <0x01c20c00 0x90>;
interrupts = <13>;
clocks = <&osc24M>;
};

wdt: watchdog@1c20ca0 {
compatible = "allwinner,suniv-f1c100s-wdt",
"allwinner,sun4i-a10-wdt";
reg = <0x01c20ca0 0x20>;
};

uart0: serial@1c25000 {
compatible = "snps,dw-apb-uart";
reg = <0x01c25000 0x400>;
interrupts = <1>;
reg-shift = <2>;
reg-io-width = <4>;
clocks = <&ccu 38>;
resets = <&ccu 24>;
status = "disabled";
};

uart1: serial@1c25400 {
compatible = "snps,dw-apb-uart";
reg = <0x01c25400 0x400>;
interrupts = <2>;
reg-shift = <2>;
reg-io-width = <4>;
clocks = <&ccu 39>;
resets = <&ccu 25>;
status = "disabled";
};

uart2: serial@1c25800 {
compatible = "snps,dw-apb-uart";
reg = <0x01c25800 0x400>;
interrupts = <3>;
reg-shift = <2>;
reg-io-width = <4>;
clocks = <&ccu 40>;
resets = <&ccu 26>;
status = "disabled";
};

// modify by kashine
mmc0: mmc@1c0f000 {
compatible = "allwinner,suniv-f1c100s-mmc",
"allwinner,sun7i-a20-mmc";
reg = <0x01c0f000 0x1000>;
clocks = <&ccu CLK_BUS_MMC0>,
<&ccu CLK_MMC0>,
<&ccu CLK_MMC0_OUTPUT>,
<&ccu CLK_MMC0_SAMPLE>;
clock-names = "ahb",
"mmc",
"output",
"sample";
resets = <&ccu RST_BUS_MMC0>;
reset-names = "ahb";
interrupts = <23>;
pinctrl-names = "default";
pinctrl-0 = <&mmc0_pins>;
status = "disabled";
#address-cells = <1>;
#size-cells = <0>;
};

// modify by kashine
fe0: display-frontend@1e00000 {
compatible = "allwinner,suniv-f1c100s-display-frontend";
reg = <0x01e00000 0x20000>;
interrupts = <30>;
clocks = <&ccu CLK_BUS_DE_FE>, <&ccu CLK_DE_FE>,
<&ccu CLK_DRAM_DE_FE>;
clock-names = "ahb", "mod",
"ram";
resets = <&ccu RST_BUS_DE_FE>;
status = "disabled";

ports {
#address-cells = <1>;
#size-cells = <0>;

fe0_out: port@1 {
#address-cells = <1>;
#size-cells = <0>;
reg = <1>;

fe0_out_be0: endpoint@0 {
reg = <0>;
remote-endpoint = <&be0_in_fe0>;
};
};
};
};

// modify by kashine
be0: display-backend@1e60000 {
compatible = "allwinner,suniv-f1c100s-display-backend";
reg = <0x01e60000 0x10000>;
reg-names = "be";
interrupts = <31>;
clocks = <&ccu CLK_BUS_DE_BE>, <&ccu CLK_DE_BE>,
<&ccu CLK_DRAM_DE_BE>;
clock-names = "ahb", "mod",
"ram";
resets = <&ccu RST_BUS_DE_BE>;
reset-names = "be";
assigned-clocks = <&ccu CLK_DE_BE>;
assigned-clock-rates = <300000000>;

ports {
#address-cells = <1>;
#size-cells = <0>;

be0_in: port@0 {
#address-cells = <1>;
#size-cells = <0>;
reg = <0>;

be0_in_fe0: endpoint@0 {
reg = <0>;
remote-endpoint = <&fe0_out_be0>;
};
};

be0_out: port@1 {
#address-cells = <1>;
#size-cells = <0>;
reg = <1>;

be0_out_tcon0: endpoint@0 {
reg = <0>;
remote-endpoint = <&tcon0_in_be0>;
};
};
};
};

// modify by kashine
pwm: pwm@1c21000 {
compatible = "allwinner,sun4i-a10-pwm";
reg = <0x01C21000 0x08>;
clocks = <&osc24M>;
#pwm-cells = <3>;
status = "disabled";
};
};
};

.dts文件做以下修改,添加了一个panel面板设备结点,对应4.3寸RGB屏幕,其compatible属性对应指定的显示驱动,我们将在后面提到;添加了backlight背光调节结点,设置了不同的背光等级;然后打开或修改了.dtsi文件中结点的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// SPDX-License-Identifier: (GPL-2.0+ OR X11)
/*
* Copyright 2018 Icenowy Zheng <icenowy@aosc.io>
*/

/dts-v1/;
#include "suniv-f1c100s.dtsi"
// modify by kashine
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/input/input.h>
#include <dt-bindings/interrupt-controller/irq.h>

/ {
model = "Lichee Pi Nano";
compatible = "licheepi,licheepi-nano", "allwinner,suniv-f1c100s";

aliases {
serial0 = &uart0;
};

chosen {
stdout-path = "serial0:115200n8";
};

// modify by kashine
panel: panel {//ampire_am800480r3tmqwa1h_mode
compatible = "alientek,alientek_4p3_inch", "simple-panel";
#address-cells = <1>;
#size-cells = <0>;
reset-gpios = <&pio 4 4 GPIO_ACTIVE_LOW>;
power-supply = <&reg_vcc3v3>;
//backlight = <&backlight>;
port@0 {
reg = <0>;
#address-cells = <1>;
#size-cells = <0>;

panel_input: endpoint@0 {
reg = <0>;
remote-endpoint = <&tcon0_out_lcd>;
};
};
};

// modify by kashine
reg_vcc3v3: vcc3v3 {
compatible = "regulator-fixed";
regulator-name = "vcc3v3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
};

// modify by kashine
backlight: backlight {
compatible = "pwm-backlight";
pwms = <&pwm 1 500000 0>;
pwm-names = "backlight";

brightness-levels = <0 4 8 16 32 64 128 255>;
default-brightness-level = <7>;
status = "okay";
};
};

&uart0 {
pinctrl-names = "default";
pinctrl-0 = <&uart0_pe_pins>;
status = "okay";
};

// modify by kashine all down
&mmc0 {
vmmc-supply = <&reg_vcc3v3>;
bus-width = <4>;
broken-cd;
status = "okay";
};

&be0 {
status = "okay";
};

&de {
status = "okay";
};

&tcon0 {
pinctrl-names = "default";
pinctrl-0 = <&lcd_rgb666_pins>;
status = "okay";
};

&tcon0_out {
tcon0_out_lcd: endpoint@0 {
reg = <0>;
remote-endpoint = <&panel_input>;
};
};

&pio {
pwm1_pin: pwm1_pin {
pins = "PE6";
function = "pwm1";
};
};

&pwm {
pinctrl-names = "default";
pinctrl-0 = <&pwm1_pin>;
status = "okay";
};

2、屏幕信息添加

本文使用正点原子40Pin RGB 屏幕,分辨率为800*480,根据正点原子提供的资料,在内核目录**/drivers/gpu/drm/panel/panel_simple.c中,填写屏幕时序信息**如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// modify by kashine
static const struct drm_display_mode alientek_4p3_inch_mode = {
.clock = 31000,
.hdisplay = 800,
.hsync_start = 800 + 40,
.hsync_end = 800 + 40 + 48,
.htotal = 800 + 40 + 48 + 88,
.vdisplay = 480,
.vsync_start = 480 + 13,
.vsync_end = 480 + 13 + 3,
.vtotal = 480 + 13 + 3 + 32,
.vrefresh = 60,
};

static const struct panel_desc alientek_4p3_inch = {
.modes = &alientek_4p3_inch_mode,
.num_modes = 1,
.bpc = 6,
.size = {
.width = 154,
.height = 85,
},
.bus_format = MEDIA_BUS_FMT_RGB666_1X18,
};

首先来解释alientek_4p3_inch_mode结构体中的参数含义,主要为屏幕的时序信息,设置错误可能导致显示错位、不完整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
clock: RGB屏幕需要使用的时钟频率
hdisplay:有效显示区水平像素数量,800*480中的800,对应Active Width
hsync_start:水平同步开始,对应hdispay + HFP
hsync_end:水平同步结束,对应hdisplay + HFP + HSYNC width(HSPW)
htotal:水平总像素,对应hdisplay + HFP + HSYNC width + HBP

vdisplay:垂直显示像素,800*480中的480,对应Active Height
vsync_start:垂直同步开始,对应vdispay + VFP
vsync_end:垂直像素结束,对应vdisplay + VFP + VSYNC width(VSPW)
vtotal:垂直总像素,对应vdisplay + VFP + VSYNC width + VBP

vrefresh:刷新率

原文链接:https://blog.csdn.net/u012794472/article/details/124868025

alientek_4p3_inch结构体主要用来描述所用屏幕的信息,包括显示模式、像素格式等。其中MEDIA_BUS_FMT_RGB666_1X18的含义详见The Linux Kernel documentationwidth、height为液晶的尺寸,需要根据屏幕的用户使用手册进行设置,如下图所示为本文使用的ATK4384屏幕尺寸图。

另一处需要修改的地方如下所示,添加设备树对应的设备驱动绑定信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	.data = &vl050_8048nt_c01,
}, {
.compatible = "winstar,wf35ltiacd",
.data = &winstar_wf35ltiacd,
}, {// modify by kashine
.compatible = "alientek,alientek_4p3_inch",
.data = &alientek_4p3_inch,
},{
/* Must be the last entry */
.compatible = "panel-dpi",
.data = &panel_dpi,
}, {
/* sentinel */
}

3、内核驱动修改

以下为核心内容,本人尝试了坑网、CSDN、荔枝派、芒果派等的众多教程,历经多次内核修改与编译,最终整理出关于F1C200s在**Linux5.7.1内核**下的RGB驱动修改方法**如下。主要修改文件为内核目录/drivers/gpu/drm/sun4i/文件夹下的三个文件,分别为sun4i_tcon.c、sun4i_drv.c、sun4i_backend.c。**以下修改内容主要是为了使驱动适配F1C200s这款芯片。

可以根据代码中注释及前后内容,定位三个文件需要修改的地方。

// sun4i_backend.c文件修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// modify by kashine 
static const struct sun4i_backend_quirks suniv_backend_quirks = {
};

static const struct sun4i_backend_quirks sun4i_backend_quirks = {
.needs_output_muxing = true,
};

static const struct sun4i_backend_quirks sun5i_backend_quirks = {
};

static const struct sun4i_backend_quirks sun6i_backend_quirks = {
};

static const struct sun4i_backend_quirks sun7i_backend_quirks = {
.needs_output_muxing = true,
.supports_lowest_plane_alpha = true,
};

static const struct sun4i_backend_quirks sun8i_a33_backend_quirks = {
.supports_lowest_plane_alpha = true,
};

static const struct sun4i_backend_quirks sun9i_backend_quirks = {
};

static const struct of_device_id sun4i_backend_of_table[] = {
{// modify by kashine
.compatible = "allwinner,suniv-f1c100s-display-backend",
.data = &suniv_backend_quirks,
},

{
.compatible = "allwinner,sun4i-a10-display-backend",
.data = &sun4i_backend_quirks,
},
{
.compatible = "allwinner,sun5i-a13-display-backend",
.data = &sun5i_backend_quirks,
},
{
.compatible = "allwinner,sun6i-a31-display-backend",
.data = &sun6i_backend_quirks,
},
{
.compatible = "allwinner,sun7i-a20-display-backend",
.data = &sun7i_backend_quirks,
},
{
.compatible = "allwinner,sun8i-a23-display-backend",
.data = &sun8i_a33_backend_quirks,
},
{
.compatible = "allwinner,sun8i-a33-display-backend",
.data = &sun8i_a33_backend_quirks,
},
{
.compatible = "allwinner,sun9i-a80-display-backend",
.data = &sun9i_backend_quirks,
},
{ }
};

// sun4i_drv.c文件修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static bool sun4i_drv_node_is_frontend(struct device_node *node)
{
// modify by kashine
return of_device_is_compatible(node, "allwinner,suniv-f1c100s-display-frontend") ||
of_device_is_compatible(node, "allwinner,sun4i-a10-display-frontend") ||
of_device_is_compatible(node, "allwinner,sun5i-a13-display-frontend") ||
of_device_is_compatible(node, "allwinner,sun6i-a31-display-frontend") ||
of_device_is_compatible(node, "allwinner,sun7i-a20-display-frontend") ||
of_device_is_compatible(node, "allwinner,sun8i-a23-display-frontend") ||
of_device_is_compatible(node, "allwinner,sun8i-a33-display-frontend") ||
of_device_is_compatible(node, "allwinner,sun9i-a80-display-frontend");
}
static const struct of_device_id sun4i_drv_of_table[] = {
{ .compatible = "allwinner,suniv-f1c100s-display-engine" },// modify by kashine
{ .compatible = "allwinner,sun4i-a10-display-engine" },
{ .compatible = "allwinner,sun5i-a10s-display-engine" },
{ .compatible = "allwinner,sun5i-a13-display-engine" },
{ .compatible = "allwinner,sun6i-a31-display-engine" },
{ .compatible = "allwinner,sun6i-a31s-display-engine" },
{ .compatible = "allwinner,sun7i-a20-display-engine" },
{ .compatible = "allwinner,sun8i-a23-display-engine" },
{ .compatible = "allwinner,sun8i-a33-display-engine" },
{ .compatible = "allwinner,sun8i-a83t-display-engine" },
{ .compatible = "allwinner,sun8i-h3-display-engine" },
{ .compatible = "allwinner,sun8i-r40-display-engine" },
{ .compatible = "allwinner,sun8i-v3s-display-engine" },
{ .compatible = "allwinner,sun9i-a80-display-engine" },
{ .compatible = "allwinner,sun50i-a64-display-engine" },
{ .compatible = "allwinner,sun50i-h6-display-engine" },
{ }
};

// sun4i_tcon.c文件修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// modify by kashine
static const struct sun4i_tcon_quirks suniv_f1c100s_quirks = {
/*
* The F1C100s SoC has a second channel in TCON, but the clock input of
* it is not documented.
*/
.has_channel_0 = true,
//.has_channel_1 = true,
.dclk_min_div = 1,// Linux5.2不需要加,5.7.1不加该代码会白屏
};

static const struct sun4i_tcon_quirks sun4i_a10_quirks = {
.has_channel_0 = true,
.has_channel_1 = true,
.dclk_min_div = 4,
.set_mux = sun4i_a10_tcon_set_mux,
};
/* sun4i_drv uses this list to check if a device node is a TCON */
const struct of_device_id sun4i_tcon_of_table[] = {
{.compatible = "allwinner,suniv-f1c100s-tcon", .data = &suniv_f1c100s_quirks },// modify by kashine
{ .compatible = "allwinner,sun4i-a10-tcon", .data = &sun4i_a10_quirks },
{ .compatible = "allwinner,sun5i-a13-tcon", .data = &sun5i_a13_quirks },
{ .compatible = "allwinner,sun6i-a31-tcon", .data = &sun6i_a31_quirks },
{ .compatible = "allwinner,sun6i-a31s-tcon", .data = &sun6i_a31s_quirks },
{ .compatible = "allwinner,sun7i-a20-tcon", .data = &sun7i_a20_quirks },
{ .compatible = "allwinner,sun7i-a20-tcon0", .data = &sun7i_a20_tcon0_quirks },
{ .compatible = "allwinner,sun7i-a20-tcon1", .data = &sun7i_a20_quirks },
{ .compatible = "allwinner,sun8i-a23-tcon", .data = &sun8i_a33_quirks },
{ .compatible = "allwinner,sun8i-a33-tcon", .data = &sun8i_a33_quirks },
{ .compatible = "allwinner,sun8i-a83t-tcon-lcd", .data = &sun8i_a83t_lcd_quirks },
{ .compatible = "allwinner,sun8i-a83t-tcon-tv", .data = &sun8i_a83t_tv_quirks },
{ .compatible = "allwinner,sun8i-r40-tcon-tv", .data = &sun8i_r40_tv_quirks },
{ .compatible = "allwinner,sun8i-v3s-tcon", .data = &sun8i_v3s_quirks },
{ .compatible = "allwinner,sun9i-a80-tcon-lcd", .data = &sun9i_a80_tcon_lcd_quirks },
{ .compatible = "allwinner,sun9i-a80-tcon-tv", .data = &sun9i_a80_tcon_tv_quirks },
{ }
};

4、开机logo显示

使用make menuconfig打开图形化配置界面,打开Bootup logo,其中的三个选项全部选中。

1
2
3
4
5
6
-> Device Drivers
-> Graphics support
-> Bootup logo (LOGO [=y])
-> Standard black and white Linux logo
-> Standard 16-color Linux logo
-> Standard 224-color Linux logo

背光选项记得勾上(其实不勾也可以)。


三、RGB屏幕显示功能测试

修改完上述内核文件后,重新编译,编译所需的时间相比于初次编译要短很多。在uboot的bootargs参数中添加如下内容****可使内核输出更多drm相关信息,****便于调试:

1
drm.debug=0x1f debug

如图所示为开发板上电启动过程中RGB屏幕显示的内容,启动时左上角显示Linux logo,符合我们的预期。

进入Debian文件系统后,可以使用如下命令查看日志的输出级别(在Linux中日志一共分为8个等级,数值越小,优先级越高)下面的四个数字是由kernel/printk.c文件中定义的一个数组决定的。

1
2
cat /proc/sys/kernel/printk
# 默认输出7 4 1 7

上面四个数字的含别如下:

控制台日志级别:优先级高于该值的消息将被打印至控制台
默认的消息日志级别:将用该优先级来打印没有优先级的消息
最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级)
默认的控制台日志级别:控制台日志级别的缺省值

如果觉得开机之后输出的日志信息太多,可以使用如下命令修改打印的日志信息级别,减少不必要的信息干扰。

1
2
# 屏蔽掉所有的内核printk打印
echo 0 4 1 7 > /proc/sys/kernel/printk

四、屏幕背光调节功能测试

通过PE6管脚输出PWM波,控制屏幕背光引脚,进而控制屏幕亮度,本文在设备树中设置了八个亮度等级,通过在脚本(.sh文件,并chomd权限)中输入以下代码可以控制亮度变化。(这里有个小问题,输入零的时候并没有熄屏,留个小坑吧)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while true
do
for i in {7..1}
do
sleep 0.2
echo $i > /sys/class/backlight/backlight/brightness
done

for j in {1..7}
do
echo $j > /sys/class/backlight/backlight/brightness
sleep 0.2
done
done

五、主要参考内容

1.RGB接口 LCD驱动适配

2.Lctech Pi(F1C200S)4.3寸(480*272)16位RGB565LCD屏驱动适配

3.解决f1c100s 主线Linux 升级到 4.19 之后的版本没有 framebuffer(fb0)设备问题(Linux5.2)

4.修改Linux kernel中打印的级别

5.f1c100s驱动rgb屏求助

6.更换正点原子7寸RGB LCD(1024*600),启动失败

7.ttyS、ttySAC、tty、ttyn的区别_HeroKern的博客-CSDN博客_ttytcu0和ttyths0的区别

GT1151触摸屏驱动

一、AT4384 RGB屏幕触摸芯片

使用正点原子屏幕的朋友一定要注意以前原子的触摸屏的触摸芯片是gt9147的,教程里的也是gt9147的驱动,后来,大概在2021年5月份前后,原子的4.3寸屏更换了触摸芯片了,触摸芯片型号是gt1151的。所以,后期买屏幕的朋友可能拿到屏幕的芯片不是gt9147的了,所以按照教程或者前面的帖子来改的话,可能是无法触摸的。所以说,一定要先确定自己触摸屏幕的触摸芯片型号。


二、设备树修改

1、.dtsi文件修改

dtsi文件中添加如下内容,在pio结点下对本次实验所用到的管脚进行功能复用,所用到的功能为I2C通信功能、GPIO输出复位功能、中断检测功能。其中,I2C选择i2c0,也就是PE11、PE12,复位管脚选择PE4,中断检测管脚选择PE3,这是我们的硬件决定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
pio: pinctrl@1c20800 {
compatible = "allwinner,suniv-f1c100s-pinctrl";
reg = <0x01c20800 0x400>;
interrupts = <38>, <39>, <40>;
clocks = <&ccu 37>, <&osc24M>, <&osc32k>;
clock-names = "apb", "hosc", "losc";
gpio-controller;
interrupt-controller;
#interrupt-cells = <3>;
#gpio-cells = <3>;

uart0_pe_pins: uart0-pe-pins {
pins = "PE0", "PE1";
function = "uart0";
};

// modify by kashine
lcd_rgb666_pins: lcd-rgb666-pins {
pins = "PD0", "PD1", "PD2", "PD3", "PD4",
"PD5", "PD6", "PD7", "PD8", "PD9",
"PD10", "PD11", "PD12", "PD13", "PD14",
"PD15", "PD16", "PD17", "PD18", "PD19",
"PD20", "PD21";
function = "lcd";
};

// modify by kashine
mmc0_pins: mmc0-pins {
pins = "PF0", "PF1", "PF2", "PF3", "PF4", "PF5";
function = "mmc0";
};

// modify by kashine 2
i2c0_pins: i2c0_pins {
pins = "PE11", "PE12";
function = "i2c0";
};

// modify by kashine 2
ts_reset_pin: ts_reset_pin {
pins = "PE4";
function = "gpio_out";
};
ts_int_pin: ts_int_pin {
pins = "PE3";
function = "gpio_in";
};

};

soc结点下添加i2c0结点, 描述了i2c0对应的驱动、时钟和所用管脚等信息,在.dtsi文件中该结点的状态属性值为disabled,这意味着需要在.dts文件中使能该结点功能。

1
2
3
4
5
6
7
8
9
10
11
12
i2c0: i2c@1C27000 {
compatible = "allwinner,sun6i-a31-i2c";
reg = <0x01C27000 0x400>;
interrupts = <7>;
clocks = <&ccu CLK_BUS_I2C0>;
resets = <&ccu RST_BUS_I2C0>;
pinctrl-names = "default";
pinctrl-0 = <&i2c0_pins>;
status = "disabled";
#address-cells = <1>;
#size-cells = <0>;
};

2、.dts文件修改

dts文件添加以下内容,gt1151触摸屏对应的驱动为**goodix,gt1151ATK4384,*对于该驱动的编写,我们将在下一节进行描述。注意:gt1151器件地址和gt9147相同,都是0x14。*每个I2C器件都有一个设备地址,通过发送具体的设备地址来决定访问哪个I2C器件。I2C设备的读写地址和I2C设备地址不同,I2C设备的读写地址是一个8位的数据,其中高7位是设备地址,最后1位是读写位,**为1的话表示这是一个读操作,为0的话表示这是一个写操作。 I2C设备的写地址 = I2C设备地址 << 1,I2C设备的读地址 = (I2C设备地址 << 1) + 1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
&i2c0 {
status = "okay";

gt1151: touchscreen@14 {
compatible = "goodix,gt1151ATK4384";// "goodix,gt1151";
reg = <0x14>;// gt1151器件地址,器件地址是I2C器件固有的地址编码,器件出厂时已经给定,不可更改
interrupt-parent = <&pio>;
interrupts = <4 3 IRQ_TYPE_EDGE_FALLING>; /* (PE3) */
pinctrl-names = "default";
pinctrl-0 = <&ts_int_pin>;
reset-gpios = <&pio 4 4 GPIO_ACTIVE_LOW>; /* RST (PE4) */
interrupt-gpios = <&pio 4 3 GPIO_ACTIVE_LOW>;
touchscreen-size-x = <800>;
touchscreen-size-y = <480>;
touchscreen-swapped-x-y;
};
};

三、驱动编写

1、驱动分析

驱动程序编写主要参考《正点原子开发指南》,在裸机开发中进行触摸屏的驱动,主要流程如下:

①、电容触摸屏是IIC接口的,需要触摸 IC,以正点原子的 ATK4384 为例,其所使用的触
摸屏控制 IC 为GT1151,因此所谓的电容触摸驱动就是 IIC设备驱动。
②、触摸IC提供了中断信号引脚(INT),可以通过中断来获取触摸信息。
③、电容触摸屏得到的是触摸位置绝对信息以及触摸屏是否有按下。
④、电容触摸屏不需要校准,当然了,这只是理论上的,如果电容触摸屏质量比较差,或
者触摸玻璃和 TFT 之间没有完全对齐,那么也是需要校准的。

那么电容触摸屏的Linux驱动主要需要以下几个驱动框架的组合:

①、IIC 设备驱动,因为电容触摸IC基本都是IIC接口的,因此大框架就是*IIC设备驱动。*
②、通过中断引脚(INT)向linux内核上报触摸信息,因此需要用到linux中断驱动框架。
标的上报在中断服务函数中完成。
③、触摸屏的坐标信息、屏幕按下和抬起信息都属于linux的input子系统,因此向 linux 内
核上报触摸屏坐标信息就得使用input子系统。

2、驱动代码

我们根据上面的分析编写了如下的gt1151的触摸屏驱动程序,至于代码的的详细解释,请参考正点原子官方文档,或者代码中注释,不在此处赘述。设备(设备树结点)和驱动匹配成功之后,gt1151probe函数就会执行,在该函数内部获取设备树终端和复位引脚、复位并初始化gt1151、获取设备信息后注册input设备,最后初始化中断。注意:设备树匹配表gt1151_of_match_table中的.compatible = “goodix,gt1151ATK4384”用来连接设备树和驱动,在设备树中的compatible一定要于此处完全相同!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
// gt1151驱动代码
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/regmap.h>
#include <linux/gpio/consumer.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/input.h>
#include <linux/input/mt.h>
#include <linux/debugfs.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/input/mt.h>
#include <linux/input/touchscreen.h>
#include <linux/i2c.h>
#include <asm/unaligned.h>

#define GT_CTRL_REG 0X8040 /* GT1151控制寄存器 */
#define GT_MODSW_REG 0X804D /* GT1151模式切换寄存器 */
#define GT_9xx_CFGS_REG 0X8047 /* GT1151配置起始地址寄存器 */
#define GT_1xx_CFGS_REG 0X8050 /* GT1151配置起始地址寄存器 */
#define GT_CHECK_REG 0X80FF /* GT1151校验和寄存器 */
#define GT_PID_REG 0X8140 /* GT1151产品ID寄存器 */

#define GT_GSTID_REG 0X814E /* GT1151当前检测到的触摸情况 */
#define GT_TP1_REG 0X814F /* 第一个触摸点数据地址 */
#define GT_TP2_REG 0X8157 /* 第二个触摸点数据地址 */
#define GT_TP3_REG 0X815F /* 第三个触摸点数据地址 */
#define GT_TP4_REG 0X8167 /* 第四个触摸点数据地址 */
#define GT_TP5_REG 0X816F /* 第五个触摸点数据地址 */
#define MAX_SUPPORT_POINTS 5 /* 最多5点电容触摸 */

// 存放电容触摸设备相关属性信息
struct gt1151_dev {
int irq_pin,reset_pin; /* 中断和复位IO */
int irqnum; /* 中断号 */
int irqtype; /* 中断类型 */
int max_x; /* 最大横坐标 */
int max_y; /* 最大纵坐标 */
void *private_data; /* 私有数据 */
struct input_dev *input; /* input结构体 */
struct i2c_client *client; /* I2C客户端 */

};
struct gt1151_dev gt1151;

const u8 irq_table[] = {IRQ_TYPE_EDGE_RISING, IRQ_TYPE_EDGE_FALLING, IRQ_TYPE_LEVEL_LOW, IRQ_TYPE_LEVEL_HIGH}; /* 触发方式 */


// 复位GT1151
static int gt1151_ts_reset(struct i2c_client *client, struct gt1151_dev *dev)
{
int ret = 0;

/* 申请复位IO*/
if (gpio_is_valid(dev->reset_pin)) {
/* 申请复位IO,并且默认输出高电平 */
ret = devm_gpio_request_one(&client->dev,
dev->reset_pin, GPIOF_OUT_INIT_HIGH,
"gt1151 reset");
if (ret) {
return ret;
}
}

/* 初始化GT1151,要严格按照GT1151时序要求 */
gpio_set_value(dev->reset_pin, 0); /* 复位GT1151 */
msleep(10);

gpio_set_value(dev->reset_pin, 1); /* 停止复位GT1151 */
msleep(10);

// gpio_set_value(dev->irq_pin, 0); /* 拉低INT引脚 */
// msleep(50);

// gpio_direction_input(dev->irq_pin); /* INT引脚设置为输入 */

return 0;
}

// 从GT1151读取多个寄存器数据
static int gt1151_read_regs(struct gt1151_dev *dev, u16 reg, u8 *buf, int len)
{
int ret;
u8 regdata[2];
struct i2c_msg msg[2];
struct i2c_client *client = (struct i2c_client *)dev->client;

/* GT1151寄存器长度为2个字节 */
regdata[0] = reg >> 8;
regdata[1] = reg & 0xFF;

// 别和SPI通信方式搞混了
/* msg[0]为发送要读取的首地址 */
msg[0].addr = client->addr; /* gt1151地址 应该是设备树中的器件地址 */
msg[0].flags = !I2C_M_RD; /* 标记为发送数据 */
msg[0].buf = &regdata[0]; /* 读取的首地址 */
msg[0].len = 2; /* reg长度*/

/* msg[1]读取数据 */
msg[1].addr = client->addr; /* gt1151地址 */
msg[1].flags = I2C_M_RD; /* 标记为读取数据*/
msg[1].buf = buf; /* 读取数据缓冲区 */
msg[1].len = len; /* 要读取的数据长度*/

ret = i2c_transfer(client->adapter, msg, 2);
if(ret == 2) {
ret = 0;
} else {
ret = -EREMOTEIO;
}
return ret;
}

// 向GT1151多个寄存器写入数据
static s32 gt1151_write_regs(struct gt1151_dev *dev, u16 reg, u8 *buf, u8 len)
{
u8 b[256];
struct i2c_msg msg;
struct i2c_client *client = (struct i2c_client *)dev->client;

b[0] = reg >> 8; /* 寄存器首地址低8位 */
b[1] = reg & 0XFF; /* 寄存器首地址高8位 */
memcpy(&b[2],buf,len); /* 将要写入的数据拷贝到数组b里面 */

msg.addr = client->addr; /* gt1151地址 */
msg.flags = 0; /* 标记为写数据 */

msg.buf = b; /* 要写入的数据缓冲区 */
msg.len = len + 2; /* 要写入的数据长度 */

return i2c_transfer(client->adapter, &msg, 1);
}

// 中断服务函数
static irqreturn_t gt1151_irq_handler(int irq, void *dev_id)
{
int touch_num = 0;
int input_x, input_y;
int id = 0;
int ret = 0;
u8 data;
u8 touch_data[5];
struct gt1151_dev *dev = dev_id;

// printk("enter irq handler!\r\n");

ret = gt1151_read_regs(dev, GT_GSTID_REG, &data, 1);// GT1151当前检测到的触摸情况
if (data == 0x00) { /* 没有触摸数据,直接返回 */
goto fail;
} else { /* 统计触摸点数据 */
touch_num = data & 0x0f;
}

/* 由于GT1151没有硬件检测每个触摸点按下和抬起,因此每个触摸点的抬起和按
* 下不好处理,尝试过一些方法,但是效果都不好,因此这里暂时使用单点触摸
*/
if(touch_num) { /* 单点触摸按下 */
gt1151_read_regs(dev, GT_TP1_REG, touch_data, 5);
id = touch_data[0] & 0x0F;
if(id == 0) { //读取成功
input_x = touch_data[1] | (touch_data[2] << 8);
input_y = touch_data[3] | (touch_data[4] << 8);

// 单点id一直等于0即可
input_mt_slot(dev->input, 0);
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, true);
input_report_abs(dev->input, ABS_MT_POSITION_X, input_x);
input_report_abs(dev->input, ABS_MT_POSITION_Y, input_y);
}
} else if(touch_num == 0){ /* 单点触摸释放 */
input_mt_slot(dev->input, id);
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, false);// 删除触摸点
}

input_mt_report_pointer_emulation(dev->input, true);// 没有出现硬件检测到的点比上报的触摸点多的情况
input_sync(dev->input);

data = 0x00; /* 向0X814E寄存器写0 */
gt1151_write_regs(dev, GT_GSTID_REG, &data, 1);

fail:
return IRQ_HANDLED;
}


// GT1151中断初始化
static int gt1151_ts_irq(struct i2c_client *client, struct gt1151_dev *dev)
{
int ret = 0;
// 由于触摸IC复位需要用到两个IO 在前面已经request了
/* 2,申请中断,client->irq就是IO中断, */
ret = devm_request_threaded_irq(&client->dev, client->irq, NULL,
gt1151_irq_handler, irq_table[dev->irqtype] | IRQF_ONESHOT,
client->name, &gt1151);
if (ret) {
dev_err(&client->dev, "Unable to request touchscreen IRQ.\n");
return ret;
}else
{
printk("irq init!\r\n");
printk("dev->irqtype = %d\r\n", dev->irqtype);
}

return 0;
}

// GT1151读取固件
static int gt1151_read_firmware(struct i2c_client *client, struct gt1151_dev *dev)
{
int ret = 0, version = 0;
u16 id = 0;
u8 data[7]={0};
char id_str[5];
ret = gt1151_read_regs(dev, GT_PID_REG, data, 6);
if (ret) {
dev_err(&client->dev, "Unable to read PID.\n");
return ret;
}
memcpy(id_str, data, 4);
id_str[4] = 0;

if (kstrtou16(id_str, 10, &id))id = 0x1001;

version = get_unaligned_le16(&data[4]);

dev_info(&client->dev, "ID %d, version: %04x\n", id, version);
switch (id) { /* 由于不同的芯片配置寄存器地址不一样需要判断一下 */
case 1151:
case 1158:
case 5663:
case 5688: /* 读取固件里面的配置信息 */
ret = gt1151_read_regs(dev, GT_1xx_CFGS_REG, data, 7);
break;
default:
ret = gt1151_read_regs(dev, GT_1xx_CFGS_REG, data, 7);
break;
}
if (ret) {
dev_err(&client->dev, "Unable to read Firmware.\n");
return ret;
}
dev->max_x = (data[2] << 8) + data[1];
dev->max_y = (data[4] << 8) + data[3];
dev->irqtype = data[6] & 0x3;
printk("X_MAX: %d, Y_MAX: %d, TRIGGER: 0x%02x", dev->max_x, dev->max_y, dev->irqtype);

return 0;
}

// probe函数
int gt1151_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
u8 data, ret;
gt1151.client = client;
printk("Driver and device has mached!!!\r\n");

/* 1,获取设备树中的中断和复位引脚 */
gt1151.irq_pin = of_get_named_gpio(client->dev.of_node, "interrupt-gpios", 0);
gt1151.reset_pin = of_get_named_gpio(client->dev.of_node, "reset-gpios", 0);

// printk("irq_pin=%d, reset_pin=%d\r\n", gt1151.irq_pin, gt1151.reset_pin);
/* 2,复位GT1151 */
ret = gt1151_ts_reset(client, &gt1151);
if(ret < 0) {
goto fail;
}

/* 3,初始化GT1151 */
data = 0x02;
gt1151_write_regs(&gt1151, GT_CTRL_REG, &data, 1); /* 软复位 */
mdelay(100);
data = 0x0;
gt1151_write_regs(&gt1151, GT_CTRL_REG, &data, 1); /* 停止软复位 */
mdelay(100);

/* 4,初始化GT1151,读取固件 */ // 应该是读触摸设备的信息
ret = gt1151_read_firmware(client, &gt1151);
if(ret != 0) {
printk("Fail !!! check !!\r\n");
goto fail;
}

/* 5,input设备注册 */
gt1151.input = devm_input_allocate_device(&client->dev);
if (!gt1151.input) {
ret = -ENOMEM;
goto fail;
}
gt1151.input->name = client->name;
gt1151.input->id.bustype = BUS_I2C;
gt1151.input->dev.parent = &client->dev;

__set_bit(EV_KEY, gt1151.input->evbit);
__set_bit(EV_ABS, gt1151.input->evbit);
__set_bit(BTN_TOUCH, gt1151.input->keybit);

input_set_abs_params(gt1151.input, ABS_X, 0, gt1151.max_x, 0, 0);
input_set_abs_params(gt1151.input, ABS_Y, 0, gt1151.max_y, 0, 0);
input_set_abs_params(gt1151.input, ABS_MT_POSITION_X,0, gt1151.max_x, 0, 0);
input_set_abs_params(gt1151.input, ABS_MT_POSITION_Y,0, gt1151.max_y, 0, 0);
ret = input_mt_init_slots(gt1151.input, MAX_SUPPORT_POINTS, 0);
if (ret) {
goto fail;
}

ret = input_register_device(gt1151.input);
if (ret)
goto fail;

/* 6,最后初始化中断 */
ret = gt1151_ts_irq(client, &gt1151);
if(ret < 0) {
goto fail;
}

return 0;

fail:
return ret;
}

// i2c驱动的remove函数
int gt1151_remove(struct i2c_client *client)
{
input_unregister_device(gt1151.input);
return 0;
}

// 传统驱动匹配表
const struct i2c_device_id gt1151_id_table[] = {
{ "goodix,gt1151ATK4384", 0, },
{ /* sentinel */ }
};


// 设备树匹配表
const struct of_device_id gt1151_of_match_table[] = {
{.compatible = "goodix,gt1151ATK4384" },
{ /* sentinel */ }
};

/* i2c驱动结构体 */
struct i2c_driver gt1151_i2c_driver = {
.driver = {
.name = "gt1151",
.owner = THIS_MODULE,
.of_match_table = gt1151_of_match_table,
},
.id_table = gt1151_id_table,
.probe = gt1151_probe,
.remove = gt1151_remove,
};

module_i2c_driver(gt1151_i2c_driver);// 展开后和module_init module_exit一样,类似于module_platform_driver

MODULE_LICENSE("GPL");
MODULE_AUTHOR("kashine");

四、内核配置

1、IIC设备使能

首先使能IIC控制器和设备树对i2c设备的支持,使用make menuconfig命令打开图形化配置界面,打开以下路径的Marvell mv64xxx I2C Controller:

Device Drivers

​ -> I2C support

​ -> I2C Hardware Bus support

全志芯片使用的是marvell的i2c控制器(参考Documentation/devicetree/bindings/i2c/i2c-mv64xxx.txt),该选项默认处于使能状态。

打开以下路径中的I2C device interface选项:

Device Drivers

​ -> I2C support

2、内核驱动添加

将上一节编写的驱动代码命名为gt1151.c,并放在linux-5.7.1/drivers/input/touchscreen目录下,如下图所示,可以看到touchscreen内有很多屏幕的适配驱动。其实gt1151官方也有适配的驱动,就是gt1151.c上方的goodix.c,这是汇顶触摸IC对应的gt系列驱动,那为什么我们还要自己编写呢?因为对于本文所用的硬件接线以及正点原子屏幕的定义,导致使用该驱动对正点原子屏幕的识别出错,得到的x、y坐标是反着的,因此需要对goodix.c进行修改,那就不如自己写喽。

仅仅将驱动文件放到对应的文件夹下面并不可以,因为内核不知道有这么个tg1151驱动文件,我们需要在touchscreen文件夹下的Makfile进行修改,在最后方添加对gt1151.c的编译请求。

1
obj-$(CONFIG_TOUCHSCREEN_GT1151)	+= gt1151.o

我们都知道在make menuconfig图形化配置界面可以对驱动进行使能、关闭、编译为模块的配置,如果想要我们的gt1151驱动编译选项能够在make menuconfig调出的图形化配置界面中配置,还需要在touchscreen文件夹下的Kconfig文件中添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
config TOUCHSCREEN_GT1151
tristate "GT1151 touchscreen controller"
depends on I2C

help
Say Y to enable support for the GT1151
family of trackpad/touchscreen controllers.

To compile this driver as a module, choose M here: the
module will be called gt1151.

endif #非添加内容!!!

3、gt1151驱动使能

添加完驱动,并提供了图形化界面配置信息后,我们还要在make menuconfig提供的图形化配置界面中使能我们添加的gt1151驱动。如下图所示,我们需要使能GT1151 touchscreen controller选项(将驱动编译进内核),也就是我们上面添加的选项。

我们自定义了gt1151的驱动,因此goodix.c中的驱动我们就不需要了,所以在相同路径中,失能goodix驱动选项****Goodix I2C touchscreen,****如下图所示:


五、触摸测试

1、启动与文件检查

经过前几节的配置,我们重新对内核和设备树进行编译,然后上电运行开发板,查看输出信息,发现内核启动时先后加载了i2c驱动和gt1151驱动,并且初始化中断功能,Debian系统启动后直接进入root用户空间。*注意:输出Driver and device has mached!!!代表设备树设备和驱动匹配成功,这是由驱动probe函数中的printk决定的!*

下面这张图是我们在 “Debian根文件系统制作”这一小节中安装的一些组件,其中evtest是为本节安装的触摸屏测试软件,当时大家可能比较疑惑,现在我们使用该软件测试触摸屏触摸是否正常,当然大家也可以使用hexdump、tslib(非常直观,我移植了两天,宣告失败,有兴趣的尝试尝试,做出来记得@我,F1C200s下tslib移植到Debian文件系统哦)。

进入root用户空间后,首先查看路径**/dev/input/event0、/dev/fb0中是否存在对应的文件,其中event0代表我们的电容触摸屏,不同的平台 event 序号不同,也可能是 event3,event4 等,**一切以实际情况为准!我的是event0,如下图所示。

如果不存在event0,或者不存在input文件夹,首先检查**/driver/input/touchscreen文件夹下面有没有产生gt1151.o文件,如果没有说明没有编译,检查Kconfig、Makfile、图形化配置使能;如果有检查compatible属性值,或是检查启动log有无输出Driver and device has mached!!!**。

2、触摸测试

很幸运你能看到这里,应该庆幸欣慰,给你点赞👍👍👍👍👍👍。在顺利完成上述操作后,我们使用evtest软件对触摸屏进行测试,使用如下命令进入测试:

1
evtest /dev/input/event0

从上面的图中可以看出,该软件正确识别到了我们的屏幕,分辨率为800*480,上报X坐标事件码为53,上报Y坐标事件码为54,现在我们来分别触摸屏幕的右下角、右上角、左下角、中间:

通过串口打印的坐标基本符合我们的触摸位置,至此,触摸IC驱动移植结束,恭喜完成本节,离offer又进一步! 🎉🎉🎉


六、主要参考内容

1.[【f1c200s/f1c100s】FT5426触摸屏驱动适配_Liangtao`的博客-CSDN博客_ft5426](https://blog.csdn.net/qq_27350133/article/details/124974526?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~Rate-1-124974526-blog-127501970.pc_relevant_multi_platform_whitelistv3&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~Rate-1-124974526-blog-127501970.pc_relevant_multi_platform_whitelistv3&utm_relevant_index=1);

2. linux设备适配触摸屏(gt1151)_星星-点灯的博客-CSDN博客

3.《正点原子驱动开发指南》

USB驱动

一、前言

1、USB host和USB device的区别

不知道你能否区分什么是USB host、什么是USB device?下面做一些简单的介绍,用最简单的语言描述大致的原理:

  • USB Host(主设备):字面意思为USB主机,****类似于电脑的USB接口,****可以连接移动硬盘、鼠标、键盘等等;
  • USB Device(从设备)****:字面意思是USB设备,概念与USB host相对,类似于我们的移动硬盘。
  • 注意:只有USB host和USB device连接时,数据才能正确传输。

那什么是USB otg呢? USB otg既可以做USB host也可以做USB device,通过ID信号来控制主、从切换。otg技术就是实现host设备不存的的情况下,设备间的数据传输。

2、硬件解析

F1C200s芯片支持USB的OTG模式,也就是可以通过更改usbid拉低或拉高方式定义当前的开发板可以作为host还是device。

  • usbid 拉高时,开发板作为外设方式。
  • usbid 拉低时,开发板作为主机方式。

F1C200s中PE2引脚具有usbid功能,来决定开发板作为外设方式或是主机方式,本文直接将PE2拉高,也即将开发板作为外设方式(device),有的朋友可能会问,*我们写USB驱动是为了外接键鼠、U盘等设备,但此处为什么作为外设方式呢?*

墨云说 ”是为了利用****sunxi-tool烧录工具,并且硬件电路里面把PE2拉高了,也就是默认是otg模式。“(本系统硬件电路主要参考墨云和稚辉君,至于这里为什么设置为外设方式,说下我的理解,大概是因为需要通过sunxi-tool将编译后的文件下载到RAM或者flash中,需要将板子作为从机,关于sunxi-tool详见sunxi-tools工具的使用)。

后记:在询问过晕哥之后,得知这个引脚其实不用拉高也可以,所以这里大家不用care,因为我们可以使用软件设置otg方式。sunxi-tool具体作用是什么?F1C200s & SD卡,能否使用这个工具?

那我们就不能将开发板作为host外接其他设备了嘛?不是。因为我们不仅可以通过硬件修改OTG模式,还可以通过软件修改。


二、设备树修改

1、.dtsi文件修改

在soc结点下添加如下两个结点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// modify by kashine 3
usb_otg: usb@1c13000 {
compatible = "allwinner,suniv-musb";
reg = <0x01c13000 0x0400>;
clocks = <&ccu CLK_BUS_OTG>;
resets = <&ccu RST_BUS_OTG>;
interrupts = <26>;
interrupt-names = "mc";
phys = <&usbphy 0>;
phy-names = "usb";
extcon = <&usbphy 0>;
allwinner,sram = <&otg_sram 1>;
status = "disabled";
};

// modify by kashine 3
usbphy: phy@1c13400 {
compatible = "allwinner,suniv-usb-phy";
reg = <0x01c13400 0x10>;
reg-names = "phy_ctrl";
clocks = <&ccu CLK_USB_PHY0>;
clock-names = "usb0_phy";
resets = <&ccu RST_USB_PHY0>;
reset-names = "usb0_reset";
#phy-cells = <1>;
status = "disabled";
};

2、.dts文件修改

使能一些功能,**USB otg设置为**主机方式,*因为我们要连接类似鼠标、键盘、U盘等的外设,*也就是开发板作为主机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// modify by kahsine 3
&otg_sram {
status = "okay";
};

// modify by kahsine 3
&usb_otg {
dr_mode = "host"; /* 三个可选项: otg / host / peripheral 我在这里指定为host模式*/
status = "okay";
};

// modify by kahsine 3
&usbphy {
usb0_id_det-gpio = <&pio 4 2 GPIO_ACTIVE_HIGH>; /* PE2 */
status = "okay";
};

三、驱动修改

1、phy-sun4i-usb.c文件修改

// 枚举变量修改

1
2
3
4
5
6
7
8
9
10
11
12
enum sun4i_usb_phy_type {
suniv_phy,// modify by kashine 3
sun4i_a10_phy,
sun6i_a31_phy,
sun8i_a33_phy,
sun8i_a83t_phy,
sun8i_h3_phy,
sun8i_r40_phy,
sun8i_v3s_phy,
sun50i_a64_phy,
sun50i_h6_phy,
};

// 结构体声明

1
2
3
4
5
6
7
8
// modify by kashine 3
static const struct sun4i_usb_phy_cfg suniv_cfg = {
.num_phys = 1,
.type = suniv_phy,
.disc_thresh = 3,
.phyctl_offset = REG_PHYCTL_A10,
.dedicated_clocks = true,
};

// compatible驱动匹配表属性修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static const struct of_device_id sun4i_usb_phy_of_match[] = {
{ .compatible = "allwinner,suniv-usb-phy", .data = &suniv_cfg }, // modify by kashine 3
{ .compatible = "allwinner,sun4i-a10-usb-phy", .data = &sun4i_a10_cfg },
{ .compatible = "allwinner,sun5i-a13-usb-phy", .data = &sun5i_a13_cfg },
{ .compatible = "allwinner,sun6i-a31-usb-phy", .data = &sun6i_a31_cfg },
{ .compatible = "allwinner,sun7i-a20-usb-phy", .data = &sun7i_a20_cfg },
{ .compatible = "allwinner,sun8i-a23-usb-phy", .data = &sun8i_a23_cfg },
{ .compatible = "allwinner,sun8i-a33-usb-phy", .data = &sun8i_a33_cfg },
{ .compatible = "allwinner,sun8i-a83t-usb-phy", .data = &sun8i_a83t_cfg },
{ .compatible = "allwinner,sun8i-h3-usb-phy", .data = &sun8i_h3_cfg },
{ .compatible = "allwinner,sun8i-r40-usb-phy", .data = &sun8i_r40_cfg },
{ .compatible = "allwinner,sun8i-v3s-usb-phy", .data = &sun8i_v3s_cfg },
{ .compatible = "allwinner,sun50i-a64-usb-phy",
.data = &sun50i_a64_cfg},
{ .compatible = "allwinner,sun50i-h6-usb-phy", .data = &sun50i_h6_cfg },
{ },
};

2、sunxi.c文件修改

// sunxi_musb_probe函数修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static int sunxi_musb_probe(struct platform_device *pdev)
{
struct musb_hdrc_platform_data pdata;
struct platform_device_info pinfo;
struct sunxi_glue *glue;
struct device_node *np = pdev->dev.of_node;
int ret;

...

glue->dev = &pdev->dev;
INIT_WORK(&glue->work, sunxi_musb_work);
glue->host_nb.notifier_call = sunxi_musb_host_notifier;

if (of_device_is_compatible(np, "allwinner,sun4i-a10-musb") || of_device_is_compatible(np, "allwinner,suniv-musb"))// modify by kashine 3
set_bit(SUNXI_MUSB_FL_HAS_SRAM, &glue->flags);

if (of_device_is_compatible(np, "allwinner,sun6i-a31-musb"))
set_bit(SUNXI_MUSB_FL_HAS_RESET, &glue->flags);

if (of_device_is_compatible(np, "allwinner,sun8i-a33-musb") ||
of_device_is_compatible(np, "allwinner,sun8i-h3-musb") ||
of_device_is_compatible(np, "allwinner,suniv-musb") ) {// modify by kashine 3
set_bit(SUNXI_MUSB_FL_HAS_RESET, &glue->flags);
set_bit(SUNXI_MUSB_FL_NO_CONFIGDATA, &glue->flags);
}

...
}

// 驱动匹配表修改

1
2
3
4
5
6
7
8
static const struct of_device_id sunxi_musb_match[] = {
{ .compatible = "allwinner,suniv-musb", }, // modify by kashine 3
{ .compatible = "allwinner,sun4i-a10-musb", },
{ .compatible = "allwinner,sun6i-a31-musb", },
{ .compatible = "allwinner,sun8i-a33-musb", },
{ .compatible = "allwinner,sun8i-h3-musb", },
{}
};

四、驱动使能

通过make menuconfig图形化配置界面,进入下方路径中使能驱动。

Device Drivers

​ -> USB support

五、U盘读取测试

重新编译,得到新的zImage、设备树,上电启动,观察输出信息,

进入Debian系统后,将U盘插入到任意一个USB接口,串口打印出下面的信息,可以看到,我们插入的3.0接口U盘容量可用大小,还需要注意一个重要的信息,我们的U盘在系统下面盘符为sda,只有一个分区sda1。

使用df -h命令查看Debian系统磁盘占用情况:

咦,怎么没有我们的U盘?因为没有挂载,😂😂😂。使用如下命令将我们的U盘挂载在**/media/UPan目录下,UPan文件夹是我们新建的哈。挂载完成重新使用df -h命令查看磁盘状况,发现了我们的U盘,也就是红色箭头**指向的内容。

1
mount /dev/sda1 /media/UPan/

进入U盘内部,测试能否正常查看其中的文件,如下图所示,U盘内的确为我备份的一些文件,一切正常。

拔出U盘之前最好先将U盘卸载(umount),使用如下命令卸载U盘后拔出即可。

有关USB驱动我们就不详细分析了(其实我不会,哈哈哈),先照葫芦画个瓢吧。USB驱动是一门很庞大、很复杂的内容,后面有时间多了解下。


六、主要参考内容

1.USB device 和 USB host区别

2. 小白自制Linux开发板 七. USB驱动配置 - 淡墨青云 - 博客园;(本文主要参bai考piao)

\3. 全志sunxi-tools烧录工具安装和使用

\4. sunxi-tool具体作用是什么?F1C200s & SD卡,能否使用这个工具?

\5. USB PHY芯片_时光-易逝的博客-CSDN博客_usb phy

音频驱动

一、前言

F1C200s是全志的一款高度集成、低功耗的移动应用处理器,可用于多种多媒体音视频设备中。

全志F1C200s基于ARM 9架构,集成了DDR。它支持高清视频解码,包括H.264、H.263、MPEG 1/2/4等。它还集成了音频编解码器和I2S/PCM接口,以增强用户体验。

由上面的介绍可以看出,F1C200s具有高清视频解码功能,因此本文使用F1C200s完成音视频播放功能。由于本文对输出音频的品质没有要求,因此本文未采用I2S声卡( I2S总线有时候也写作 IIS,I2S 是飞利浦公司提出的一种用于数字音频设备之间进行音频数据传输的总线),直接使用F1C200s自带的声卡,注意,F1C200s自带的声卡比较差劲,对声音品质要求较高请勿采用此方案。

既然音频CODEC的本质是ADC和DAC,那么采样率和采样位数就是衡量一款音频CODEC最重要的指标。比如常见音频采样率有8K、44.1K、48K、192K甚至384K和768K,采样位数常见的有8位、16位、24位、32位。

– 正点原子

理解一下上面的概念,后面会提到:

采样位数——可以理解数字音频设备处理声音的解析度,即对声音的辨析度。就像表示颜色的位数一样(8位表示256种颜色,16位表示65536种颜色),有8位,16位,24位等。这个数值越大,解析度就越高,录制和回放的声音就越真实。

采样频率——就是对声音信息1秒钟采样多少次,以记录成数字信息。如CD音频是44.1KHz采样率,它对声音以每秒44100次的频率来记录信息。原则上采样率越高,声音的质量越好。


二、驱动修改

接下来在Linux内核目录中替换下面补丁包中的代码,本补丁包在Linux5.7.1下测试成功,其他版本请备份源码后尝试,这里的代码是通过改造sun4i-codec解码方案而来。

– 墨云

具体修改内容详见https://lkml.org/lkml/2018/12/2/259,或直接使用墨云提供的内核补丁包(这是Allwinner suniv F1C100s支持DMA和音频解码器的RFC补丁),相关讨论详见此处请问有没有大神搞定了f1c100s的主线kernel4.19下的声卡驱动?,本文直接使用墨云提供的补丁包。

Suniv F1C100s具有与sun4i非常相似的DMA。但也有一些不同之处。 Suniv在时钟控制单元中有一个DMA复位位。它有更小的DMA通道数。几个寄存器有不同的地址。 它的最大突发大小是 4而不是8,添加一个 quirk 字段以区分它们。DMA端点编号也不同。


三、设备树修改

1、.dtsi文件修改

首先在.dtsi文件中添加头文件的引用:

1
#include <dt-bindings/dma/sun4i-a10.h> 

然后在soc结点下添加如下结点,F1C200s利用DMA通道发送和接收ADC-DAC样本,所以需要DMA支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dma: dma-controller@1c02000 {
compatible = "allwinner,suniv-f1c100s-dma";
reg = <0x01c02000 0x1000>;
interrupts = <18>;
clocks = <&ccu CLK_BUS_DMA>;
resets = <&ccu RST_BUS_DMA>;
#dma-cells = <2>;
};

codec: codec@1c23c00 {
compatible = "allwinner,suniv-f1c100s-codec";
reg = <0x01c23c00 0x400>;
interrupts = <21>;
clocks = <&ccu CLK_BUS_CODEC>,
<&ccu CLK_CODEC>;
clock-names = "apb", "codec";
resets = <&ccu RST_BUS_CODEC>;
dmas = <&dma SUN4I_DMA_NORMAL 0x0c>,
<&dma SUN4I_DMA_NORMAL 0x0c>;
dma-names = "rx", "tx";
status = "disabled";
};

2、.dts文件修改

Allwinner suniv F1C200s 现在具有基本的音频编解码器支持,在设备树dts文件中为Lichee Pi Nano开发板激活它。

1
2
3
4
5
6
7
&codec {
allwinner,audio-routing =
"Headphone", "HP",
"Headphone", "HPCOM",
"MIC", "Mic";
status = "okay";
};

四、驱动使能

使用make menuconfig图形化配置驱动,进入如下路径,使能对应的驱动。注意:这两个驱动默认处于使能状态。

Device Drivers

​ -> Sound card support

​ -> Advanced Linux Sound Architecture

​ -> ALSA for SoC audio support

​ -> Allwinner SoC Audio support(使能该驱动)

Device Drivers

​ -> DMA Engine support(使能)


五、启动测试与配置

1、启动与检查

重新编译,并上电,可以看到F1c100s Audio Codec 的声卡配置如下:

使用如下命令检查声卡,确实是否正确启动,如下图所示为正常启动:

1
cat /proc/asound/cards

2、默认声卡配置

使用如下命令打开asound.conf文件输入下方内容,Debian文件系统中可能不存在这个文件,自行创建即可。

1
2
3
4
vi /etc/asound.conf
defaults.ctl.card 1
defaults.pcm.card 1
defaults.timer.card 1

如果未正确配置默认声卡,播放mp3文件的时候可能报错如下:


六、音频视频播放测试

1、mplayer与alsa-utils安装

使用如下命令在我们的开发板中(如果已经配置好无线网卡),或者是qemu模拟器中安装两个软件:

1
2
sudo apt-get install mplayer
sudo apt-get install alsa-utils

qemu模拟器中安装mplayer报错如下,经过后续测试并不影响使用。

2、音频播放测试

使用如下命令播放准备好的mp3文件,等待输出以下内容后开始播放音乐,按0键增加音量,按9键减小音量。经测试,音频播放正常。

1
mplayer audio.mp3

3、视频播放测试

本文使用mplayer进行视频播放测试,使用以下命令查看驱mplayer的帮助信息,从帮助信息中可以看出,***-vo选项*是用来选择*视频输出驱动的。*

使用命令mplayer -vo help显示支持的视频驱动,本文使用fbdev2进行播放(因为只有fbdev2才能播放,不清楚墨云为什么使用fbdev可以播放)。

由于F1C200s的性能有限(实在是太有限了),视频的分辨率和帧率已经很低很低了,仍然提示系统性能不支持播放较高分辨率的视频,如果在项目中使用,建议直接I2S,外接音频解码芯片。如果你所使用的芯片性能比较差的话mplayer会给你提示如下:

本文选用分辨率400x240、帧率5帧的视频,其音频采样率为24000,音频质量为64k,****注意视频分辨率不能大于800x480,也就是屏幕分辨率,使用如下命令播放,同样是按0键增加音量,按9键减小音量。

1
mplayer -vo fbdev2 400x240_5_24000_64k.mp4 

经测试视频正常播放,视频正常播放时日志输出如下:


七、音频输入测试

输入alsamixer命令得到以下图形界面配置,**(这里还有一点需要补充,按说应该在上一小节说明的,第二个Headphone选项一定要选择DAC,否则无法正常输出音视频),**按下F4**进入Capture**配置。(F1是帮助)

Mic Boost设置为100,ADC Mixer Mic捕获使能,或者通过amixer -c 1 cset numid=14 on命令使能捕获。

使用areconrd -l命令列出捕获硬件设备列表,很明显我们要使用card 1。

根据arecord录音_zkw_1998的博客-CSDN博客_arecord没有声音所说的内容,并参考如何测试音频驱动比如录音,播放声音?,如下图所示,指定设备为card 1,设置录音格式为wav,录音10秒。

根据上面分析得到录音指令为:

1
arecord -D "plughw:1,0" -f S16_LE -r 16000 -d 10 -t wav file.wav

录音完毕,使用mplay file.wav进行播放。经测试,一切正常。

注:音频播放延迟问题解决方法:**#27楼解决**

V3s linux 4.13 音频播放延迟了,开始以为是线程同步问题,纠结了很久


八、主要参考内容

1.为了精华特意制作F1c200s-MP4播放器,支持硬件H264(开源板子和原理给个精华) / 全志 SOC / WhyCan Forum(哇酷开发者社区)

\2. 荔枝派音频驱动 / 全志 SOC / WhyCan Forum(哇酷开发者社区)

3.探讨一下全志芯片的音频接口硬件电路连接应该怎么使用最好 / 全志 SOC / WhyCan Forum(哇酷开发者社区)

4.请教一下V3S怎么外接扬声器 / 全志 SOC / WhyCan Forum(哇酷开发者社区)

5.白嫖党的胜利!开源F1C200S桌面小音箱!【验证通过】 / 全志 SOC / WhyCan Forum(哇酷开发者社区)

6.V3S CODEC的使用及驱动测试(声音播放功能) / 全志 SOC / WhyCan Forum(哇酷开发者社区)

\7. [请问有没有大神搞定了f1c100s的主线kernel4.19下的声卡驱动?13楼@wammaw1314搞定声卡驱动,欢迎测试] / 全志 SOC / WhyCan Forum(哇酷开发者社区)

8.[慢更]小白探索如何使用V3s播放音乐 / 全志 SOC / WhyCan Forum(哇酷开发者社区)

9.mplayer 移植 - Tankiii - 博客园

10.V3s linux 4.13 音频播放延迟了,开始以为是线程同步问题,纠结了很久 (问题由 @Andy1234 解决,欢迎大家验证) / 全志 SOC / WhyCan Forum(哇酷开发者社区)

11.小白自制Linux开发板 八. Linux音频驱动配置 - 淡墨青云 - 博客园

12.各侠大神,v3s BSP内核 如何测试音频驱动比如录音,播放声音? / 全志 SOC / WhyCan Forum(哇酷开发者社区)

13.arecord录音_zkw_1998的博客-CSDN博客_arecord没有声音

14.https://lkml.org/lkml/2018/12/2/259。

WiFi驱动

一、ESP-12F作无线网卡

本文重点参考众人拾柴-F1C200S通过SPI使用ESP8089或ESP8266做无线网卡四. 通过SPI使用ESP8266做无线网卡。通过使用去掉Flash的ESP-12F作为F1C200s的无线网卡,使F1C200s获得访问外网的能力。本文配置无线网卡驱动,在对ESP-12F复位之后,利用F1C200s通过SPI通信将**固件**下载*到ESP-12F,并通过SPI接口进行网络数据传输*(我的理解,如有错误,欢迎评论区指正)。至于本文的硬件电路,详见本专栏的第一篇文章。

Linux驱动有两种运行方式,第一种就是将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用“insmod”命令或“modprobe”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。

再次强调:如果使用ESP-12F模块,请务必去掉模块中的Flash芯片!


二、模块化WiFi驱动

本节首先下载ESP8089无线驱动源码(ESP12-F同样适用),对源码进行修改后编译生成模块化驱动(.ko文件),在Debian文件系统中加载该驱动,解决加载过程中出现的一系列问题,最终配置网络,成功ping通百度。

驱动的移植工作主要是:

  • 配置复位引脚,完成对ESP-12F的复位;
  • 配置SPI通信,完成固件下载与网络通信;
  • 配置中断引脚,辅助通信。

1、无线驱动源码下载

在此处ESP8089-SPI/README.md at master · notabucketofspam/ESP8089-SPI · GitHub下载esp8089无线网卡驱动,(是的我们又是用的别人写好的驱动,慢慢来吧,先按照步骤做出来现象,后面理解原理),并在Linux5.7.1源码根目录创建spiwifi文件夹,将下载的源码放到该文件夹中。

2、无线驱动源码适配

首先修改无线驱动项目的**KBUILD**使其指向 Linux-5.7.1内核构建树的路径(*内核源码目录*)。KBUILD变量主要用来生成一些和汇编有关的文件(Kconfig是图形界面的描述文件)。并且添加架构和交叉编译器信息,至于本文为何使用arm-linux-gnueabi-编译器,**怎么和迪卡、墨云等人的不同,原因见这里F1C200s无线网卡问题源码编译?

1
2
3
4
5
# modify by kashine 5
# KBUILD ?= $(shell readlink -f /lib/modules/$(KVERS_UNAME)/build)
KBUILD ?= /home/project01/pro01/new_kernel/linux-5.7.1/
ARCH ?= arm
CROSS_COMPILE ?= arm-linux-gnueabi-

修改KBUILD、ARCH、CROSS_COMPILE之后,使用make命令进行编译。然而并没有像迪卡那样报错:

1
2
*** WARNING: This kernel lacks wireless extensions.
Wireless drivers will not work properly.

为了不影响后续的操作,我们同样进行对****内核源码目录*中的*/net/wireless/Kconfig文件*开头部分进行以下修改*:

1
2
3
4
config WIRELESS_EXT
bool
#更改为
def_bool y

前面我们提到需要对ESP12F进行复位,并且需要中断支持,一方面需要配置设备树,另一方面需要对驱动源码进行修改。在无线驱动源码目录中的spi_stub.c文件中进行如下修改,管脚编号对应关系为:PA0 – 0;PB0 – 32;PC0 – 64;PD0 – 96;PE0 – 128。

修改完成以后,使用重新编译,编译输出log如下,警告可以忽略(变量声明位置不合适):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
make -C /home/project01/pro01/new_kernel/linux-5.7.1/ M=/home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github
make[1]: Entering directory '/home/project01/pro01/new_kernel/linux-5.7.1'
AR /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/built-in.a
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_debug.o
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/sdio_sif_esp.o
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.o
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_io.o
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_file.o
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_main.o
In file included from ./include/linux/mm_types.h:12:0,
from ./include/linux/mmzone.h:21,
from ./include/linux/gfp.h:6,
from ./include/linux/slab.h:15,
from ./include/linux/crypto.h:19,
from ./include/crypto/hash.h:11,
from ./include/linux/uio.h:10,
from ./include/linux/socket.h:8,
from ./include/linux/compat.h:15,
from ./include/linux/ethtool.h:17,
from ./include/linux/netdevice.h:37,
from /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_main.c:17:
/home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_main.c: In function ‘esp_pub_init_all’:
./include/linux/completion.h:54:2: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
struct completion work = COMPLETION_INITIALIZER(work)
^
./include/linux/completion.h:74:43: note: in expansion of macro ‘DECLARE_COMPLETION’
# define DECLARE_COMPLETION_ONSTACK(work) DECLARE_COMPLETION(work)
^~~~~~~~~~~~~~~~~~
/home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_main.c:81:2: note: in expansion of macro ‘DECLARE_COMPLETION_ONSTACK’
DECLARE_COMPLETION_ONSTACK(complete);
^~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_main.c:221:0:
/home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/eagle_fw1.h: In function ‘esp_download_fw’:
/home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/eagle_fw1.h:8:1: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
static u8 eagle_fw1[] =
^~~~~~
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_sip.o
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_ext.o
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_ctrl.o
/home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_ctrl.c: In function ‘sip_send_ampdu_action’:
/home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_ctrl.c:479:29: warning: this statement may fall through [-Wimplicit-fallthrough=]
action->ssn = ssn;
~~~~~~~~~~~~^~~~~
/home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_ctrl.c:480:9: note: here
case SIP_AMPDU_RX_STOP:
^~~~
/home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_ctrl.c:481:31: warning: this statement may fall through [-Wimplicit-fallthrough=]
action->index = index;
~~~~~~~~~~~~~~^~~~~~~
/home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_ctrl.c:482:9: note: here
case SIP_AMPDU_TX_OPERATIONAL:
^~~~
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_mac80211.o
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_utils.o
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp_pm.o
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/testmode.o
LD [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp8089-spi.o
MODPOST 1 modules
CC [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp8089-spi.mod.o
LD [M] /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/esp8089-spi.ko
make[1]: Leaving directory '/home/project01/pro01/new_kernel/linux-5.7.1'

3、设备树修改

添加SPI管脚复用spi0结点,注意在dtsi中未对spi0进行使能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 在pio分组添加
spi0_pc_pins: spi0-pc-pins {
pins = "PC0","PC1","PC2","PC3";
function = "spi0";
};


// 在soc分组下添加
spi0:spi@1c05000 {
compatible = "allwinner,suniv-spi",
"allwinner,sun8i-h3-spi";
reg = <0x01c05000 0x1000>;
interrupts = <10>;
clocks = <&ccu CLK_BUS_SPI0>, <&ccu CLK_BUS_SPI0>;
clock-names = "ahb", "mod";
resets = <&ccu RST_BUS_SPI0>;
status = "disabled";
#address-cells = <1>;
#size-cells = <0>;
pinctrl-names = "default";
pinctrl-0 = <&spi0_pc_pins>;
};

在dts文件中对spi0结点进行使能,**为什么没有添加中断复位引脚呢?*因为我们下载的源码是板级描述文件*(大概意思是,驱动源码和设备树没有关系,直接在代码中获取硬件信息**),直接在源码中指定即可,也就是上一小节指定的中断和复位引脚。

1
2
3
&spi0 {
status = "okay";
};

需要注意的一点是,我们需要在使能SPI驱动配置,在.config中保证如下驱动处于使能状态:

1
2
3
4
5
CONFIG_SPI=y
CONFIG_SPI_MASTER=y
CONFIG_SPI_SUN4I=y
CONFIG_SPI_SPIDEV=y
CONFIG_SPI_SUN6I=y

此处有个坑,我们在配置.config文件的时候,一定要删除对应的注释,比如我们要配置使能CONFIG_SPI_ALTERA,那么一定要将对应的注释“CONIFG_SPI_ALTERA is not set”删除,否则可能无法成功使能对应驱动。详见warning: override: reassigning to symbol 问题解决。

4、模块驱动加载

将编译产生的esp8089-spi.ko驱动文件拷贝到Debian根文件系统(TF卡rootfs分区)的**/lib/*modules/5.7.1文件夹下,如果文件夹路径不存在需要手动创建对应的文件夹,文件夹名字一定要下配置正确,5.7.1是内核版本号,需要根据实际情况修改,当然,重新编译过的Linux内核镜像文件设备树文件也需要更新。注意:本文直接使用的Debian根文件系统,因为buildroot制作的根文件系统各种命令都缺,而buildroot编译过于繁琐耗时。*

📌 模块驱动加载命令

更新对应的文件之后,上电启动,使用ls命令可以看到我们编译产生的驱动文件。我们首先说明模块驱动的加载方式:

驱动编译完成以后扩展名为.ko,有两种命令可以**加载驱动模块:*insmod和modprobe。* **insmod 命令不能解决模块的依赖关系,*比如 drv.ko 依赖 first.ko 这个模块,就必须先使用insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。而modprobe 会分析模块的依赖关系,*然后会将所有的依赖模块都加载到内核中,modprobe 命令默认会去/lib/modules/目录中查找模块。

另外有一点需要注意,使用modprobe命令之前需要使用depmod命令,depmod命令可检测模块的相依性,供modprobe在安装模块时使用。

📌 驱动加载、遇到的问题、解决方法

进入**/lib/modules/5.7.1文件夹,使用depmod**命令检测驱动模块相依性,但是会报错如下,提示没有对应的文件(挠头,怎么会没有文件呢?)。

参考这个博客depmod: ERROR: could not open directory /lib/modules/5.2.0-licheepi-zero+: No such file or dir,将内核中的modules.order和modules.builtin这两个文件拷贝到根文件系统的**/lib/modules/5.7.1**文件夹中,这两个文件在内核源码根目录下,如下图所示:

复制完成,重新上电启动,在**/lib/modules/5.7.1文件夹使用depmod命令不再出现报错,使用depmod完成模块相依性检测后,通过modprobe命令加载驱动,需注意的是,以下两个命令只有后者才可以正常执行。**

1
2
modprobe esp-8089.ko # 无法正常运行
modprobe esp-8089 # 可正常运行

📌 驱动问题解决

modprobe执行成功,但是我们的驱动并没有正确加载,细心的朋友可以发现在加载完驱动之后,打印出以下信息,这说明我们的驱动代码既没有找到SPI主设备,有没有创建SPI从设备,这显然是不允许的,因为SPI是我们进行无线通信的基础。

1
2
esp8089_spi: FAILED to find master
esp8089_spi: FAILED to create slave

经过迪卡大佬的排查,发现是无线驱动代码中spi_stub.c文件出错:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct spi_device* sif_platform_new_device(void) {
master = spi_busnum_to_master(esp_board_spi_devices[0].bus_num);
if(!master)
printk("esp8089_spi: FAILED to find master\n");// 报错
spi = spi_new_device( master, esp_board_spi_devices );
if(!spi)
printk("esp8089_spi: FAILED to create slave\n");// 报错
printk("esp8089_spi: I will go dead\n");
if(spi_setup(spi))
printk("esp8089_spi: FAILED to setup slave\n");
printk("esp8089_spi: I am OK\n");
return spi;
}

也就是说下面的代码出了问题:

1
master = spi_busnum_to_master(esp_board_spi_devices[0].bus_num);

其中,esp_board_spi_devices[]spi_stub.c文件前面定义的结构体:

1
2
3
4
5
6
7
8
9
static struct spi_board_info esp_board_spi_devices[] = {
{
.modalias = "ESP8089_0",
.max_speed_hz = MAX_SPEED_HZ,
.bus_num = 1,
.chip_select = 0,
.mode = 0,
},
};

迪卡分析:

bus_num就是1;spi_busnum_to_master(1)研究一下;每个master都对应一个bus num。
注册spi slave设备(ESP-12F SPI),由dts解析得到,dts会指定spi slave挂载在哪个bus num下,由bus num就可以得到对应的spi master 了。(可能不合理,但能跑)

****将bus_num改为0后,**重新编译无线驱动代码并上电运行,**打印信息如下:

奇怪不奇怪,我们的spi0分明没有作为其他设备的通信接口,但是报错信息提示我们片选地址已被使用。

墨云分析:

可见spi_master已经注册成功,但是chipselect 0 already in use,说明当前配置SPI0,中片选为0的地址已经被使用,实时上我们并未链接其他设备,所以怀疑是其他问题,通过查找资料SPI通信模式分为4中模式,经过逐一测试发现SPI_MODE_3也就是(4)可用。

根据墨云的分析对无线驱动代码spi_stub.c文件中的代码进行修改如下:

1
2
3
4
5
6
7
8
9
static struct spi_board_info esp_board_spi_devices[] = {
{
.modalias = "ESP8089_0",
.max_speed_hz = MAX_SPEED_HZ,
.bus_num = 0,// modify by kashine 5
.chip_select = 0,
.mode = SPI_MODE_3,// modify by kashine 5
},
};

修改后重新编译,上电后加载esp-8089驱动。****注意,如果使用我的硬件电路,一定要注意,如果屏幕较大,并且同时使用无线模块,可能导致供电不足,现象是屏幕熄灭、串口关闭,解决办法是除串口USB之外,利用USB HUB扩展的USB接口增加一个USB供电。**模块驱动加载日志**如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
root@likaiqin-virtual-machine:/lib/modules/5.7.1# modprobe esp8089-spi
[ 128.493238] esp8089_spi: loading out-of-tree module taints kernel.
[ 128.508273] esp8089_spi: EAGLE DRIVER VER bdf5087c3deb
[ 129.114478] esp8089_spi: esp_spi_dummy_probe enter
[ 129.120111] esp8089_spi: register board OK
[ 129.124623] esp8089_spi: sem_timeout = 0
[ 129.340023] esp8089_spi: ESP8089 power up OK
[ 129.345448] esp8089_spi: esp_spi_probe ENTER
[ 129.350322] esp8089_spi: esp_setup_spi
[ 129.354477] esp8089_spi: sif_spi_protocol_init
[ 129.359437] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1559
[ 129.370920] esp8089_spi: fail_count = 0
[ 129.498009] rx:[0x00],[0x00],[0x00],[0x00],[0x00],[0x00],[0x00],[0x00],[0x00],[0x00]
[ 129.606625] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1559
[ 129.618134] esp8089_spi: fail_count = 1
[ 129.733228] rx:[0x3f],[0x09],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 129.841860] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1559
[ 129.853379] esp8089_spi: fail_count = 2
[ 130.054228] rx:[0xff],[0xff],[0x01],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 130.661815] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1578
[ 130.679097] rx:[0xff],[0xff],[0x01],[0x10],[0xff],[0xff],[0x00],[0xff],[0xff],[0xff]
[ 131.186870] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1591
[ 131.230265] rx:[0xff],[0xff],[0x00],[0x90],[0xff],[0xff],[0x00],[0xff],[0xff],[0xff]
[ 131.751505] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1603
[ 131.795833] rx:[0xff],[0x00],[0x02],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 132.317567] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1617
[ 132.363805] rx:[0xff],[0x00],[0x03],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 132.886662] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1630
[ 132.935628] rx:[0xff],[0x00],[0x02],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 133.459909] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1643
[ 133.512035] rx:[0xff],[0x00],[0x03],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 134.037354] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1655
[ 134.091882] rx:[0xff],[0xff],[0x00],[0x00],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 134.617589] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1655
[ 134.672142] rx:[0xff],[0xff],[0x00],[0x25],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 135.198185] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1655
[ 135.254763] rx:[0xff],[0xff],[0x00],[0x10],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 135.780766] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1655
[ 135.834891] rx:[0xff],[0xff],[0x00],[0x12],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 136.361421] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1655
[ 136.416215] rx:[0xff],[0xff],[0x00],[0x00],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 136.942723] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1655
[ 136.997550] rx:[0xff],[0xff],[0x00],[0x06],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 137.523992] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1655
[ 137.578214] rx:[0xff],[0xff],[0x00],[0x00],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 138.104701] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1655
[ 138.159169] rx:[0xff],[0xff],[0x00],[0x00],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 138.685676] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1668
[ 138.740390] rx:[0xff],[0x00],[0x00],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 138.767739] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1681
[ 138.834001] rx:[0xff],[0x00],[0x02],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 138.861304] esp8089_spi: /home/project01/pro01/new_kernel/linux-5.7.1/spiwifi/ESP8089-SPI-github/spi_sif_esp.c, 1694
[ 138.963902] rx:[0xff],[0x00],[0x01],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff],[0xff]
[ 139.998233] esp8089_spi: esp_pub_init_all
[ 140.021398] esp8089_spi: esp_download_fw
[ 140.459848] esp8089_spi: sif_platform_irq_init enter
[ 150.889369] resetting event timeout
[ 150.910574] esp8089_spi: esp_init_all failed: -110
[ 150.932966] esp8089_spi: first error exit
[ 150.954067] esp8089_spi: esp_spi_probe EXIT
[ 150.975268] esp8089_spi: sem_timeout = 0
[ 150.995481] esp8089_spi: esp_spi_init err 0
root@likaiqin-virtual-machine:/lib/modules/5.7.1#

从上面的驱动加载日志中可以就看出两个信息一个是固件下载成功,一个是超时问题。

针对超时问题,采用迪卡的屏蔽操作,相当于强制跳过超时警告执行。修改无线驱动源码的****esp_sip.c文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
extern struct task_struct *sif_irq_thread;// 声明变量
sip_poll_bootup_event(struct esp_sip *sip)
{
int ret = 0;

esp_dbg(ESP_DBG_TRACE, "polling bootup event... \n");

if (gl_bootup_cplx)
ret = wait_for_completion_timeout(gl_bootup_cplx, 2 * HZ);

esp_dbg(ESP_DBG_TRACE, "******time remain****** = [%d]\n", ret);
if (ret <= 0) {
esp_dbg(ESP_DBG_ERROR, "bootup event timeout\n");

// modify by kashine 5
// return -ETIMEDOUT;
sip->epub->wait_reset = 0;
wake_up_process(sif_irq_thread);
esp_dbg(ESP_DBG_ERROR, "for unknow reason,we may not be informed the boot/rst complete event, assume it completed and continue here\n");
msleep(50);
}

if(sif_get_ate_config() == 0){
ret = esp_register_mac80211(sip->epub);
}
sip_poll_resetting_event(struct esp_sip *sip)
{
int ret = 0;

esp_dbg(ESP_DBG_TRACE, "polling resetting event... \n");

if (gl_bootup_cplx)
ret = wait_for_completion_timeout(gl_bootup_cplx, 10 * HZ);

esp_dbg(ESP_DBG_TRACE, "******time remain****** = [%d]\n", ret);
if (ret <= 0) {
esp_dbg(ESP_DBG_ERROR, "resetting event timeout\n");
// modify by kashine 5
// return -ETIMEDOUT;
sip->epub->wait_reset = 0;
wake_up_process(sif_irq_thread);
esp_dbg(ESP_DBG_ERROR, "for unknow reason,we may not be informed the boot/rst complete event, assume it completed and continue here\n");
msleep(50);
}

esp_dbg(ESP_DBG_TRACE, "target resetting %d %p\n", ret, gl_bootup_cplx);

修改完成,重新编译上电启动,加载驱动后, 输出日志如下(部分):

5、启动网卡

至此,需要加载esp8089无线驱动,然后使用命令ifconfig wlan0 up启动网卡:

使用ifconfig查看网卡:

6、连接互联网

Debian根文件系统制作的时候,我们已经安装了对应的网络组件,此处我们使用组件wpa_supplicant连接wifi,wifi为我的手机热点。在Debian根文件系统的/etc目录下创建配置文件,并按照以下格式输入wifi信息:

1
2
3
4
5
vi /etc/wpa_supplicant.conf
network={
ssid="wifi名称"
psk="wifi密码"
}

然后执行如下指令进行wifi连接:

1
wpa_supplicant -B -d -i wlan0 -c /etc/wpa_supplicant.conf

**执行*udhcpc -i wlan0*命令,获取IP地址:

ping一下百度试试:

7、apt-get命令测试

之前我们在制作Debian根文件系统的时候,安装过evtest触摸屏测试软件,现在我们把它卸载掉,然后重新安装,以此测试apt-get安装软件是否正常。首先使用dpkg –list查看是否安装了evtest软件。

如果安装了evtest软件,使用apt-get remove命令卸载,在卸载过程中,弹出了一堆的报错信息如下,我们来分析一下,systemd-journal invoked oom-killer,提示进程被杀掉,OOM killer(Out Of Memory killer),该机制会监控那些占用内存过大,尤其是瞬间占用内存很快的进程,防止内存耗尽而自动把该进程杀掉。*这样看应该是内核检测到系统内存不足、挑选并杀掉某个进程。*

为什么会内存不足呢?DDR有64M,我们在制作根文件系统的时候添加了swap分区,将一部分硬盘作为虚拟内存,使用free命令查看一下内存的使用情况,咦,swap分区怎么是0?后来发现我多次制作根文件系统,在重新制作根文件系统的时候没有添加swap分区。

按照Debian根文件系统制作博客的教程重新建立swap分区,如下:

重新卸载evtest软件,不再报错。

使用dpkg –list命令查看是否成功卸载,发现不存在evtest软件,正常卸载。

重新安装evtest软件,正常安装。至此,F1C200s无线网卡驱动完成,可以直接使用apt-get命令安装软件了,不必在Ubuntu下使用qemu模拟器安装。

8、文件传输

如何在虚拟机中的Ubuntu和我们的开发板之间传输文件呢?

首先保证我们的物理机、Ubuntu虚拟机、开发板在同一个热点的同一个网段下,可以相互ping通,如下图所示,这里我就不再赘述,详细内容参考linux开发板访问互联网 笔记本win10中虚拟机_Kashine的博客-CSDN博客

为了防止混淆,使用vi /etc/hostname命令修改主机名,将主机Ubuntu命名为Ubuntu,开发板命名为:Debian。

Debian和Ubuntu都要打开允许ssh以root登录, 使用 vi /etc/ssh/sshd_config命令打开文件写入PermitRootLogin yes,如果未使能服务端SSH服务配置了root用户登录策略,会产生“Permission denied, please try again.”的报错提示。

配置完成后,使用如下命令进入Debian:

1
2
#ssh root@Debian ip
ssh root@192.168.180.164

可以在Ubuntu中控制Debian播放mp3文件或者是mp4文件:

跑题了,跑题了,怎么放音乐看视频去了,大年初一可以放松一下,哈哈。来看文件传输,首先使用exit命令退出上面的远程访问。如果欲将Ubuntu中的文件复制到Debian中,应该怎么操作呢?在Debian中使用指令scp root@192.168.180.164(Ubuntu的ip):/Ubuntu中文件路径 /拷贝到Debian中的位置可以将Ubuntu中文件拷贝到Debian中。可以看到传输速度为734.4KB/s还可以吧,说的过去,整个过程不消耗流量哦(我是用的手机热点)。

1
scp root@192.168.180.59:/mnt/hgfs/Desktop/opencv3.4.13bit32.rar ./

至此, 本文的需求全部解决,至于墨云所说开发板只要大量发送数据,比如作为Web服务器被访问,wifi就挂了,本文测试了在Ubuntu下将Debian中的文件拷贝到Ubuntu,也就是开发板发送数据,并未出现报错或者wifi断开的情况。

我错了我错了,又试了一次,wifi挂了。

Debian接收数据经过连续多次测试正常。

经连续多次测试发现发送数据也没有问题,bug无法复现,可能是发送的文件太小,随缘解决吧。

至此,无线网卡功能全部实现,但我们发现每次上电之后都需要加载驱动,很是麻烦,能不能直接将驱动编译到内核呢?当然可以。


三、内核WiFi驱动

1、内核驱动代码下载

首先在F1C200S修改ESP8089源码,由原来板极描述文件改为设备树,一键配置

或者墨云提供的下载链接https://files.cnblogs.com/files/twzy/esp8089.zip(复制到地址栏下载)下载**内核wifi驱动代码源码,**其实此源码**大部分和上面模块化驱动相同,**只是此处使用设备树获取设备信息,将驱动文件**直接编译到内核。**下载的代码也是在上面的基础上修改而来的,详见第一个链接。

2、设备树修改

设备树修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
&spi0 {
status = "okay";
esp8089@0 {
status = "okay";
compatible = "boss,esp8089";
spi-cpol;
spi-cpha;
reg = <0>;
spi-max-frequency = <30000000>;
reset= <135>; //PE7
interrupt= <136>; //PE8
debug= <0>;
};
};
spi0:spi@1c05000 {
compatible = "allwinner,suniv-spi",
"allwinner,sun8i-h3-spi";
reg = <0x01c05000 0x1000>;
interrupts = <10>;
clocks = <&ccu CLK_BUS_SPI0>, <&ccu CLK_BUS_SPI0>;
clock-names = "ahb", "mod";
resets = <&ccu RST_BUS_SPI0>;
status = "disabled";
#address-cells = <1>;
#size-cells = <0>;
pinctrl-names = "default";
pinctrl-0 = <&spi0_pc_pins>;
};

3、内核无线驱动源码修改

首先将内核无线驱动源码复制到内核源码的/drivers/sta****ging/目录下,因为我们使用的是SPI0,因此无线驱动源码中的spi_stub.c文件需要修改如上一小节所示。

然后在spi_stub.c文件对我们使用的管脚进行修改,需要修改吗?是不需要的,我们之前对管脚进行修改是因为我们用的spi_stub.c文件中的变量描述硬件信息,现在我们使用设备树描述硬件设备信息,我们下载的代码中已经集成设备树解析函数,因此我们无需再次在代码中指定硬件信息。

加入防止超时的代码:

1
extern struct task_struct *sif_irq_thread;

修改完成使用make命令进行编译,发现以下错误,提示的意思是函数本来是void类型,结果我们按照有返回值的形式使用函数,esp_debug.c文件中的内容进行修改:

esp_debug.c文件修改完成如下所示,重新使用make命令编译内核,不再报错。

4、无线网卡测试

编译完成,更新内核镜像文件,上电启动,内核自动加载无线驱动,无需手动modprobe esp8089.ko,进入Debian系统后,依次输入如下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ifconfig wlan0 up #启动网卡
ifconfig #查看现有网卡

vi /etc/wpa_supplicant.conf # 配置WiFi信息

network={
ssid="热点"
psk="密码"
}

wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf

udhcpc -i wlan0

ping www.baidu.com

成功ping通百度,此次连接的不是手机热点,而是WiFi,延迟略微降低。对于文件传输等请自行测试,本文不再赘述。


四、参考内容

\1. 众人拾柴-F1C200S通过SPI使用ESP8089或ESP8266做无线网卡 / 全志 SOC / WhyCan Forum(哇酷开发者社区)

\2. 小白自制Linux开发板 四. 通过SPI使用ESP8266做无线网卡 - 淡墨青云 - 博客园

\3. [ESP8089-SPI移植支持设备树]F1C200S修改ESP8089源码,由原来板极描述文件改为设备树,一键配置。

4.i.MX6ULL Linux阿尔法开发板 — 正点原子资料下载中心 1.0.0 文档

\5. depmod: ERROR: could not open directory /lib/modules/5.2.0-licheepi-zero+: No such file or dir

6.modprobe: FATAL: Module xxx.ko not found in directory /lib/modules/$(uname -r)_SoldierJazz2021的博客-CSDN博客

7.warning: override: reassigning to symbol 问题解决_爱就是恒久忍耐的博客-CSDN博客_reassigning to symbol

8.腾讯云的轻量应用服务器修改主机名hostname的方法 - 知乎

9.使用ssh 连接linux 并传送文件_SIXTOME的博客-CSDN博客_linux ssh 传文件

NES游戏移植

一、前言

经过前面十篇文章,我们的小电脑现拥有自己的Debian系统,可以进行屏幕显示与触摸,可以播放音频、视频,可以连接键盘、U盘等设备,也可以连接无线络下载软件、远程访问文件传输等等。虽然小电脑功能已经非常齐全了,但是我们它还是个空空荡荡的小电脑,我们是不是应该娱乐一下、放松一下呢?必须滴啊,开始整活。


二、环境搭建

1、GCC、G++编译器安装

通过上一篇文章的学习,我们可以使用无线网进行软件安装,使用apt-get命令****安装gcc、g++编译器:

安装完成,使用gcc -v验证是否安装成功,如下图所示为安装成功输出日志。

2、音频组件alsa-utils安装

移植NES游戏机模拟器程序,这里使用InfoNes,InfoNES音频部分需要alsa相关的组件。

前面我们已经安装过alsa-utils,此处就无需安装,如果前面没有安装,使用如下指令安装组件。

3、其他组件安装

make组件安装,安装完成后

1
2
apt-get install libsdl1.2-dev # 漫长的等待
apt-get install libasound2-dev # 可能不需要安装

三、NES模拟器编译

编译NES,进入NES源码/arm-NES-linux-master/linux/文件夹下(使用墨云提供的源码,详细修改时内容详见墨云),使用make命令进行编译,然后是漫长的等待。编译完成后,在当前目录下生成InfoNES文件,这便是我们的模拟器。

自行准备.nes后缀的游戏文件,想必能够做到此处的朋友们找个nes游戏应该不成问题,将NES游戏复制到Debian的/home/用户名目录中,使用如下命令启动模拟器打开游戏:

1
./InfoNES ../../hundouluo.nes # /arm-NES-linux-master/linux所在目录

然后就是娱乐时刻:


四、参考内容

\1. 小白自制Linux开发板 十. NES游戏玩起来 - 淡墨青云 - 博客园

【项目原理】多点触摸屏驱动原理

一、屏幕介绍

ATK-7016 这款屏幕其实是由 TFT LCD+触摸屏组合起来的。底下是 LCD 面板,上面是触摸面板,将两个封装到一起就成了带有触摸屏的 LCD 屏幕。电容触摸屏也是需要一个驱动 IC的,驱动 IC 一般会提供一个 I2C 接口给主控制器,主控制器可以通过 I2C 接口来读取驱动 IC里面的触摸坐标数据。ATK-7016、ATK-7084 这两款屏幕使用的触摸控制 IC 是 FT5426,ATK-4342 使用的驱动 IC 是 GT9147,ATK-4384 使用的驱动 IC 是 GT1151。这些电容屏触摸 IC 都是 I2C 接口的,使用方法基本一样。

ATK-4384 的电容触摸屏部分有 4 个 IO 用于连接主控制器:SCL、SDA、RST 和 INT,SCL 和 SDA 是 I2C 引脚,RST 是复位引脚,INT 是中断引脚。一般通过 INT 引脚来通知主控制器有触摸点按下,然后在 INT 中断服务函数中读取触摸数据。也可以不使用中断功能,采用轮询的方式不断查询是否有触摸点按下,本章实验我们使用中断方式来获取触摸数据。


二、触摸驱动分析

1、驱动框架分析

按照https://blog.csdn.net/qq_41709234/article/details/128661071的说法:

驱动程序编写主要参考《正点原子开发指南》,在裸机开发中进行触摸屏的驱动,主要流程如下:

①、电容触摸屏是IIC接口的,需要触摸 IC,以正点原子的 ATK4384 为例,其所使用的触摸屏控制 IC 为GT1151,因此所谓的电容触摸驱动就是 IIC设备驱动。
②、触摸IC提供了中断信号引脚(INT),可以通过中断来获取触摸信息。
③、电容触摸屏得到的是触摸位置绝对信息以及触摸屏是否有按下。
④、电容触摸屏不需要校准,当然了,这只是理论上的,如果电容触摸屏质量比较差,或者触摸玻璃和 TFT 之间没有完全对齐,那么也是需要校准的。

那么电容触摸屏的Linux驱动主要需要以下几个驱动框架的组合:

①、IIC 设备驱动,因为电容触摸IC基本都是IIC接口的,因此大框架就是IIC设备驱动。
②、通过中断引脚(INT)向linux内核上报触摸信息,因此需要用到linux中断驱动框架。坐标的上报在中断服务函数中完成。
③、触摸屏的坐标信息、屏幕按下和抬起信息都属于linux的input子系统,因此向 linux 内核上报触摸屏坐标信息就得使用input子系统。

2、多点触摸(MT)协议详解

MT 协议隶属于 linux的 input 子系统,驱动通过大量的 ABS_MT 事件向 linux 内核上报多点触摸坐标数据。根据触摸 IC 的不同,分为 Type A 和 Type B 两种类型,目前使用最多的是 Type B 类型。

老版本的 linux 内核是不支持多点电容触摸的(Multi-touch,简称 MT),MT 协议是后面加入的,因此如果使用 2.x 版本 linux 内核的话可能找不到 MT 协议。

触摸点的信息通过一系列的 ABS_MT 事件(有的资料也叫消息)上报给 linux 内核,只有ABS_MT 事件是用于多点触摸的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
852 #define ABS_MT_SLOT        0x2f /* MT slot being modified */ 
853 #define ABS_MT_TOUCH_MAJOR 0x30 /* Major axis of touching ellipse */
854 #define ABS_MT_TOUCH_MINOR 0x31 /* Minor axis (omit if circular) */
855 #define ABS_MT_WIDTH_MAJOR 0x32 /* Major axis of approaching ellipse */
856 #define ABS_MT_WIDTH_MINOR 0x33 /* Minor axis (omit if circular) */
857 #define ABS_MT_ORIENTATION 0x34 /* Ellipse orientation */
858 #define ABS_MT_POSITION_X 0x35 /* Center X touch position */
859 #define ABS_MT_POSITION_Y 0x36 /* Center Y touch position */
860 #define ABS_MT_TOOL_TYPE 0x37 /* Type of touching device */
861 #define ABS_MT_BLOB_ID 0x38 /* Group a set of packets as a blob */
862 #define ABS_MT_TRACKING_ID 0x39 /* Unique ID of initiated contact */
863 #define ABS_MT_PRESSURE 0x3a /* Pressure on contact area */
864 #define ABS_MT_DISTANCE 0x3b /* Contact hover distance */
865 #define ABS_MT_TOOL_X 0x3c /* Center X tool position */
866 #define ABS_MT_TOOL_Y 0x3d /* Center Y tool position */

🎍 TypeA

对于 Type A 类型的设备,通过 input_mt_sync()函数来隔离不同的触摸点数据信息:

void input_mt_sync(struct input_dev *dev)

该函数会触发 SYN_MT_REPORT 事件,此事件会通知接收者获取当前触摸数据,并且准备接收下一个触摸点数据。

不管是哪个类型的设备,最终都要调用 input_sync()函数来标识多点触摸信息传输完成,告诉接收者处理之前累计的所有消息,并且准备好下一次接收。

对于 Type A 类型的设备,发送触摸点信息的时序如下所示,这里以 2 个触摸点为例:

1
2
3
4
5
6
7
ABS_MT_POSITION_X x[0]// ABS_MT_POSITION_X 、 ABS_MT_POSITION_Y事件
ABS_MT_POSITION_Y y[0]// input_report_abs来完成坐标上报
SYN_MT_REPORT// input_mt_sync()函数会触发SYN_MT_REPORT事件
ABS_MT_POSITION_X x[1]
ABS_MT_POSITION_Y y[1]
SYN_MT_REPORT
SYN_REPORT

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static irqreturn_t st1232_ts_irq_handler(int irq, void *dev_id)
{
...
// 获取所有触摸点的信息
ret = st1232_ts_read_data(ts);
if (ret < 0)
goto end;

/* multi touch protocol */
for (i = 0; i < MAX_FINGERS; i++) {
if (!finger[i].is_valid)
continue;

// 按照TypeA时序上报所有触摸点的信息
input_report_abs(input_dev, ABS_MT_TOUCH_MAJOR, finger[i].t);/ ABS_MT_TOUCH_MAJOR常数用于表示触摸接触区域的主轴长度
input_report_abs(input_dev, ABS_MT_POSITION_X, finger[i].x);
input_report_abs(input_dev, ABS_MT_POSITION_Y, finger[i].y);
input_mt_sync(input_dev);
count++;
}


/* SYN_REPORT */
input_sync(input_dev);


end:
return IRQ_HANDLED;
}

🎍 TypeB

Type B 使用 slot 协议区分具体的触摸点,Type B 设备驱动需要给每个识别出来的触摸点分配一个 slot,后面使用这个 slot 来上报触摸点信息。可以通过 slot 的 ABS_MT_TRACKING_ID 来新增、替换或删除触摸点。

ABS_MT_TRACKING_ID跟踪ID:一个非负数的 ID 表示一个有效的触摸点,-1 这个 ID 表示未使用 slot。一个以前不存在的 ID 表示这是一个新加的触摸点,一个 ID 如果再也不存在了就表示删除了。

上报触摸点信息的时候需要通过 input_mt_slot()函数区分是哪一个触摸点:

void input_mt_slot(struct input_dev *dev, int slot)

该函数会触发 ABS_MT_SLOT 事件,此事件会告诉接收者当前正在更新的是哪个触摸点(slot)的数据。

不管是哪个类型的设备,最终都要调用 input_sync()函数来标识多点触摸信息传输完成,告诉接收者处理之前累计的所有消息,并且准备好下一次接收。

对于 Type B 类型的设备,发送触摸点信息的时序如下所示,这里以 2 个触摸点为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
ABS_MT_SLOT 0// 使用 input_mt_slot 函数上报当前触摸点的 ABS_MT_SLOT 事件,触摸IC提供
ABS_MT_TRACKING_ID 45// input_mt_report_slot_state完成 Type B 的要求,即每个 SLOT 必须关联一个ABS_MT_TRACKING_ID
ABS_MT_POSITION_X x[0]
ABS_MT_POSITION_Y y[0]
ABS_MT_SLOT 1
ABS_MT_TRACKING_ID 46
ABS_MT_POSITION_X x[1]
ABS_MT_POSITION_Y y[1]
SYN_REPORT// 当所有的触摸点坐标都上传完毕以后就得发送 SYN_REPORT 事件,使用 input_sync 函数来完成。

// 当一个触摸点移除以后
ABS_MT_TRACKING_ID -1 // 负1表示这个ID不使用SLOT,同样使用input_mt_report_slot_state函数,第三个参数为false即可,无需手动置为-1
SYN_REPORT// 使用input_sync函数表示上报结束

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static void ili210x_report_events(struct input_dev *input, const struct touchdata *touchdata)
{
int i;
bool touch;
unsigned int x, y;
const struct finger *finger;

// 循环上报所有触摸点
for (i = 0; i < MAX_TOUCHES; i++) {
input_mt_slot(input, i);// ABS_MT_SLOT事件

finger = &touchdata->finger[i];

touch = touchdata->status & (1 << i);
input_mt_report_slot_state(input, MT_TOOL_FINGER, touch);// 上报ABS_MT_TRACKING_ID事件,也就是给 SLOT 关联一个ABS_MT_TRACKING_ID。
if (touch) {
x = finger->x_low | (finger->x_high << 8);
y = finger->y_low | (finger->y_high << 8);

input_report_abs(input, ABS_MT_POSITION_X, x);
input_report_abs(input, ABS_MT_POSITION_Y, y);
}
}

input_mt_report_pointer_emulation(input, false);
input_sync(input);// 上报SYN_REPORT事件
}

🎍 MT触摸协议A、B总结

🎍 疑问及解答

1.TypeA和TypeB两种类型的区别如下:

Type A:适用于触摸点不能被区分或者追踪,此类型的设备上报原始数据
Type B:适用于有硬件追踪并能区分触摸点的触摸设备,此类型设备都通过slot更新某一个触摸点的信息

2.什么是slot?什么是ID?

  1. slot:是一个用于存储触摸点信息的容器,包括触摸点的ID和位置信息。每个触摸点都会被分配一个slot,以便系统能够追踪多个触摸点。
  2. ID:是触摸点的唯一标识符,用于区分不同的触摸点。系统可以通过ID来追踪触摸点的移动。

总之:

ID:用于跟踪触摸点的唯一标识符。当用户触摸屏幕时,系统会为每个触摸点分配一个唯一的 ID。

slot:每个 Slot 包含了触摸点的各种信息,如坐标(X 和 Y)、面积、压力等。

3.MT多点触摸协议中TypeB中的slot,按下屏幕上的按键,分配一个slot和一个唯一的ID,在不抬起的情况下移动这个点,slot和ID会发生什么变化?

  1. 该触摸点所占用的slot和ID不会发生改变。它会继续使用原来分配给它的那个slot和ID。
  2. 其他新的触摸点如果产生,会分配新的未使用的slot和新的唯一ID。
  3. 如果原来的某个触摸点被抬起,其所占用的slot和ID将被释放,供新触摸点使用。
  4. 如果某个触摸点由于移动越过屏幕边缘被结束,其所占用的slot和ID也将被释放。

4.MT多点触摸协议中TypeB,如果首先按下一个点,再按下一个点,再次按下一个点,然后抬起第二个按下的点,再按下一个新的点,slot和ID如何发生变化?

  1. 首先按下第一个点,分配slot 1和 ID 1;
  2. 按下第二个点,分配slot 2和 ID 2;
  3. 按下第三个点,如果slot 1和2都被占用,会分配slot 3和 ID 3;
  4. 抬起第二个点(ID 2),释放其slot 2;
  5. 按下新的第四个点,由于slot 2已空闲,会分配slot 2和新的唯一ID,例如 ID 4。

*5.为什么需要ID和slot配合,单独使用slot或者ID为什么不能完成多点触摸功能呢?*

  1. slot 数量是有限的,并且slot的使用可以确保每个触摸点都分配一个独占的通道来传输触摸相关信息但是,(原因解释),如果在同一帧内,其中4个点(A,B,C和D)持续活动,我们可能会分配slot1-4。如果我们增加了第5个和第6个点E和F,slot 5和6也可能被分配,超出原有范围,在这种情况下,slot冲突就可能发生。另一方面,如果在后续帧中,部分点(例如B和D)不再活动,但新的点(G和H)出现,我们会释放slot 2和4,并分配新的slot 5和6。此时,slot 映射会变得非常混乱和不直观。很难通过slot数来确定某个点的信息通道,也就是说,我想对第二个按下的点进行操作,但是slot2并不对应第二个按下的点。
  2. ID只是用来标识每个触摸点的唯一标志符,并不能告诉我们这些触摸点的位置、状态或其他属性。为了实现多点触摸识别功能,我们需要使用ID和其他信息(如坐标、状态、时间戳等)来跟踪和识别每个触摸点,并进行相应的操作。

6.什么情况下需要使用 input_mt_report_pointer_emulation() 函数禁止指针模拟器?

  1. 硬件设备跟踪到的触摸点数量多于正在上报的触摸点数量,启用指针模拟器可能导致触摸点数量不匹配、误判触摸点位置等问题。
  2. 应用程序需要精确地跟踪多个触摸点的位置和状态,禁用指针模拟器可以确保触摸点信息能够准确地传递到应用程序中,而不受指针模拟器的影响。
  3. 一些应用程序需要使用指针模拟器来模拟单点触控,禁用指针模拟器可能会导致这些应用程序无法正常工作。
  4. 在特殊的应用场景下,例如绘图或游戏等需要高精度触摸控制的应用程序中,禁用指针模拟器可以提高触摸点的准确性和响应性。

三、多点触摸API函数

1、input_mt_init_slots 函数

用于初始化 MT 的输入 slots,编写 MT 驱动的时候必须先调用此函数初始化 slots:

int input_mt_init_slots( struct input_dev *dev,
unsigned int num_slots,
unsigned int flags)

dev: MT 设备对应的 input_dev,因为 MT 设备隶属于 input_dev。
num_slots:多点触控设备支持的最大触摸点数量。
flags:其他一些 flags 信息,可设置的 flags 如下所示:

#define INPUT_MT_POINTER 0x0001 /* pointer device, e.g. trackpad /
#define INPUT_MT_DIRECT 0x0002 /
direct device, e.g. touchscreen /
#define INPUT_MT_DROP_UNUSED 0x0004 /
drop contacts not seen in frame /
#define INPUT_MT_TRACK 0x0008 /
use in-kernel tracking /
#define INPUT_MT_SEMI_MT 0x0010 /
semi-mt device, finger count handled manually */

可以采用‘|’运算来同时设置多个 flags 标识。
返回值:0,成功;负值,失败。

2、input_mt_slot 函数

此函数用于 Type B 类型,此函数用于产生 ABS_MT_SLOT 事件,告诉内核当前上报的是哪个触摸点的坐标数据:

void input_mt_slot(struct input_dev *dev,
int slot)

dev: MT 设备对应的 input_dev。
slot:当前发送的是哪个 slot 的坐标信息,也就是哪个触摸点。
返回值:无。

3、input_mt_report_slot_state 函数

此函数用于 Type B 类型,用于产生 ABS_MT_TRACKING_ID 和 ABS_MT_TOOL_TYPE事 件 , ABS_MT_TRACKING_ID 事 件 给 slot 关 联 一 个 ABS_MT_TRACKING_ID ,ABS_MT_TOOL_TYPE 事件指定触摸 型(是笔还是手指等):

void input_mt_report_slot_state( struct input_dev *dev,
unsigned int tool_type,
bool active)

dev: MT 设备对应的 input_dev。
tool_type:触摸类型,可以选择 MT_TOOL_FINGER(手指)、MT_TOOL_PEN(笔)或
MT_TOOL_PALM(手掌),对于多点电容触摸屏来说一般都是手指。
active:true,连续触摸,input 子系统内核会自动分配一个 ABS_MT_TRACKING_ID 给slot。
false,触摸点抬起,表示某个触摸点无效了,input 子系统内核会分配一个-1 给 slot,表示触摸点溢出。
返回值:无。

4、input_report_abs 函数

Type A 和 Type B 类型都使用此函数上报触摸点坐标信息,通过 ABS_MT_POSITION_X 和ABS_MT_POSITION_Y 事件实现 X 和 Y 轴坐标信息上报。

void input_report_abs( struct input_dev *dev,
unsigned int code,
int value)

dev: MT 设备对应的 input_dev。
code:要上报的是什么数据,可以设置为 ABS_MT_POSITION_X 或ABS_MT_POSITION_Y,也就是 X 轴或者 Y 轴坐标数据。
value:具体的 X 轴或 Y 轴坐标数据值。
返回值:无。

5、input_mt_report_pointer_emulation 函数

如果追踪到的触摸点数量多于当前上报的数量,驱动程序使用 BTN_TOOL_TAP 事件来通知用户空间当前追踪到的触摸点总数量,然后调用 input_mt_report_pointer_emulation 函数将 use_count 参数设置为 false。否则的话将 use_count 参数设置为 true,表示当前的触摸点数量(此函数会获取到具体的触摸点数量,不需要用户给出):

void input_mt_report_pointer_emulation(struct input_dev *dev,
bool use_count)
dev: MT 设备对应的 input_dev。
use_count:true,有效的触摸点数量;false,追踪到的触摸点数量多于当前上报的数量。
返回值:无。


四、多点电容触摸驱动框架分析

1、概述

①、多点电容触摸芯片的接口,一般都为 I2C 接口,因此驱动主框架肯定是 I2C。
②、linux 里面一般都是通过中断来上报触摸点坐标信息,因此需要用到中断框架。
③、多点电容触摸属于 input 子系统,因此还要用到 input 子系统框架。
④、在中断处理程序中按照 linux 的 MT 协议上报坐标信息。

2、I2C框架

I2C框架类似于platform框架,比较好理解,如果实在是不明白请看正点原子驱动指南I2C部分。当设备树中触摸 IC的设备节点和驱动匹配以后,gt1151_probe 函数就会执行,我们可以在此函数中初始化触摸 IC,中断和 input 子系统等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// probe函数
int gt1151_probe(struct i2c_client *client, const struct i2c_device_id *id)
{

}

// i2c驱动的remove函数
int gt1151_remove(struct i2c_client *client)
{
input_unregister_device(gt1151.input);
return 0;
}

// 传统驱动匹配表
const struct i2c_device_id gt1151_id_table[] = {
{ "goodix,gt1151ATK4384", 0, },
{ /* sentinel */ }
};


// 设备树匹配表
const struct of_device_id gt1151_of_match_table[] = {
{.compatible = "goodix,gt1151ATK4384" },
{ /* sentinel */ }
};

/* i2c驱动结构体 */
struct i2c_driver gt1151_i2c_driver = {
.driver = {
.name = "gt1151",
.owner = THIS_MODULE,
.of_match_table = gt1151_of_match_table,
},
.id_table = gt1151_id_table,
.probe = gt1151_probe,
.remove = gt1151_remove,
};

module_i2c_driver(gt1151_i2c_driver);// 展开后和module_init module_exit一样,类似于module_platform_driver

MODULE_LICENSE("GPL");
MODULE_AUTHOR("kashine");

2、初始化触摸IC、中断、input子系统初始化

初始化触摸芯片,包括芯片的相关 IO,比如复位、中断等 IO 引脚,然后就是芯片本身的初始化,也就是配置触摸芯片的相关寄存器。

设置 input_dev 需要上报的事件为 EV_ABS 和 BTN_TOUCH,因为多点电容屏的触摸坐标为绝对值,因此需要上报 EV_ABS 事件。触摸屏有按下和抬起之分,因此需要上报 BTN_TOUCH 按键。

调用 input_set_abs_params 函数设置 EV_ABS 事件需要上报 ABS_X、ABS_Y、ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// probe函数
int gt1151_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
u8 data, ret;
gt1151.client = client;
printk("Driver and device has mached!!!\r\n");

/* 1、初始化触摸IC */
/* 获取设备树中的中断和复位引脚 */
gt1151.irq_pin = of_get_named_gpio(client->dev.of_node, "interrupt-gpios", 0);
gt1151.reset_pin = of_get_named_gpio(client->dev.of_node, "reset-gpios", 0);

// printk("irq_pin=%d, reset_pin=%d\r\n", gt1151.irq_pin, gt1151.reset_pin);
/* 复位GT1151 */
ret = gt1151_ts_reset(client, &gt1151);
if(ret < 0) {
goto fail;
}

/* 初始化GT1151 */
data = 0x02;
gt1151_write_regs(&gt1151, GT_CTRL_REG, &data, 1); /* 软复位 */
mdelay(100);
data = 0x0;
gt1151_write_regs(&gt1151, GT_CTRL_REG, &data, 1); /* 停止软复位 */
mdelay(100);

/* 初始化GT1151,读取固件 */ // 应该是读触摸设备的信息
ret = gt1151_read_firmware(client, &gt1151);
if(ret != 0) {
printk("Fail !!! check !!\r\n");
goto fail;
}

/* 2、input设备申请与初始化 */
gt1151.input = devm_input_allocate_device(&client->dev);
if (!gt1151.input) {
ret = -ENOMEM;
goto fail;
}
gt1151.input->name = client->name;
gt1151.input->id.bustype = BUS_I2C;
gt1151.input->dev.parent = &client->dev;

__set_bit(EV_KEY, gt1151.input->evbit);// 支持按键事件
__set_bit(BTN_TOUCH, gt1151.input->keybit);// 设置按键位图为抬起和按下之分的按键
__set_bit(EV_ABS, gt1151.input->evbit);// 支持绝对坐标事件

/* 设置 EV_ABS 事件需要上报 ABS_X、ABS_Y、ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y。 */
input_set_abs_params(gt1151.input, ABS_X, 0, gt1151.max_x, 0, 0);
input_set_abs_params(gt1151.input, ABS_Y, 0, gt1151.max_y, 0, 0);
input_set_abs_params(gt1151.input, ABS_MT_POSITION_X,0, gt1151.max_x, 0, 0);
input_set_abs_params(gt1151.input, ABS_MT_POSITION_Y,0, gt1151.max_y, 0, 0);
ret = input_mt_init_slots(gt1151.input, MAX_SUPPORT_POINTS, 0);// 初始化slot
if (ret) {
goto fail;
}

/* 注册input_dev */
ret = input_register_device(gt1151.input);
if (ret)
goto fail;

/* 3、最后初始化中断 */
ret = gt1151_ts_irq(client, &gt1151);
if(ret < 0) {
goto fail;
}

return 0;

fail:
return ret;
}

3、上报坐标信息

进入中断处理程序以后首先肯定是从触摸 IC 里面读取触摸坐标以及触摸点数量,假设触摸点数量保存到 num 变量,触摸点坐标存放到 x,y 数组里面。

每一轮触摸点坐标上报完毕以后就调用一次 input_sync 函数发送一个SYN_REPORT 事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 中断服务函数
static irqreturn_t gt1151_irq_handler(int irq, void *dev_id)
{
int touch_num = 0;
int input_x, input_y;
int id = 0;
int ret = 0;
u8 data;
u8 touch_data[5];
struct gt1151_dev *dev = dev_id;

// printk("enter irq handler!\r\n");

ret = gt1151_read_regs(dev, GT_GSTID_REG, &data, 1);// GT1151当前检测到的触摸情况
if (data == 0x00) { /* 没有触摸数据,直接返回 */
goto fail;
} else { /* 统计触摸点数据 */
touch_num = data & 0x0f;
}

/* 由于GT1151没有硬件检测每个触摸点按下和抬起,因此每个触摸点的抬起和按
* 下不好处理,尝试过一些方法,但是效果都不好,因此这里暂时使用单点触摸
*/
if(touch_num) { /* 单点触摸按下 */
gt1151_read_regs(dev, GT_TP1_REG, touch_data, 5);
id = touch_data[0] & 0x0F;
if(id == 0) { //读取成功
input_x = touch_data[1] | (touch_data[2] << 8);
input_y = touch_data[3] | (touch_data[4] << 8);

// 单点id一直等于0即可
input_mt_slot(dev->input, 0);
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, true);
input_report_abs(dev->input, ABS_MT_POSITION_X, input_x);
input_report_abs(dev->input, ABS_MT_POSITION_Y, input_y);
}
} else if(touch_num == 0){ /* 单点触摸释放 */
input_mt_slot(dev->input, id);
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, false);// 删除触摸点
}

input_mt_report_pointer_emulation(dev->input, true);// 没有出现硬件检测到的点比上报的触摸点多的情况
input_sync(dev->input);

data = 0x00; /* 向0X814E寄存器写0 */
gt1151_write_regs(dev, GT_GSTID_REG, &data, 1);

fail:
return IRQ_HANDLED;
}

项目原理】DRM驱动概念、组成、框架、源码分析

一、DRM与Framebuffer

Linux 中的 DRM(Direct Rendering Manager) 驱动和 Framebuffer (fbdev) 驱动是两种不同的图形驱动方式,它们之间有一些区别。

Framebuffer驱动:

  • 直接控制显卡的帧缓冲区,提供基本的显卡输出功能;
  • 使用一些内核数据结构和API来管理图形界面,并提供一组接口与用户空间的应用程序进行通信;
  • 相对简单,适合于嵌入式系统或者不需要高性能图形的应用场景。

DRM驱动:

  • 提供一种分离的图形驱动架构,将硬件驱动程序、内核模块和用户空间驱动程序进行分离;
  • 支持多个应用程序同时访问显卡,并提供了更丰富的图形功能,例如硬件加速和3D加速;
  • 提供了一些内核接口,可以让用户空间应用程序与驱动程序进行交互;
  • 支持多显示器和多GPU的配置。

总之,一句话,DRM是Linux目前主流的图形显示框架,相比FB架构,DRM更能适应当前日益更新的显示硬件。尽管FB退出历史舞台,在DRM 中也并未将其遗弃,而是集合到DRM中,供部分嵌入式设备使用。


二、DRM驱动

linux驱动系列学习之DRM(十)_linux drm_紫川宁520的博客-CSDN博客

1、概述

DRM从模块上划分,可以简单分为3部分:libdrm、KMS、GEM。

libdrm(DRM框架在用户空间的Lib)

libdrm是一个用户空间的DRM库,提供了DRM驱动的用户空间接口。用户或应用程序在用户空间调用libdrm提供的库函数, 即可访问到显示的资源,并对显示资源进行管理和使用。

KMS(内核显示模式设置)

KMS属于DRM框架下的一个大模块,主要负责两个功能:显示参数及显示控制。 这两个基本功能可以说是显示驱动必须具备的能力,在DRM框架下, 为了将这两部分适配得符合现代显示设备逻辑,又分出了几部分子模块配合框架(CRTC,ENCODER,CONNECTOR,PLANE,FB,VBLANK,property)

更新画面:显示buffer的切换,多图层的合成方式,以及每个图层的显示位置。
设置显示参数:包括分辨率、刷新率、电源状态(休眠唤醒)等。

GMS(图形执行管理器)

它提供了一种*抽象的**显存管理***方式**(主要负责显示buffer的分配和释放)**,使用户空间应用程序可以更方便地管理显存,而不需要了解底层硬件的细节。(DUMB、PRIME、fence)**

基本元素

DRM框架涉及到的元素很多,大致如下:

KMS:CRTC,ENCODER,CONNECTOR,PLANE,FB,VBLANK,property

GEM:DUMB、PRIME、fence

学习DRM驱动其实就是学习上面各个元素的实现及用法,如果你能掌握这些知识点,那么在编写DRM驱动的时候就能游刃有余。

Linux DRM(二)基本概念和特性_Younix脏羊的博客-CSDN博客

元素 说明
CRTC 电子束控制器,用于控制显卡输出信号,将帧缓存中的图像数据按照一定的方式输出到显示器上,并控制显示器的显示模式、分辨率、刷新率等参数。在DRM里有多个显存,可以通过操作CRTC来控制要显示的那个显存。
ENCODER 编码器,用于将 CRTC 输出的图像信号转换成一定格式的数字信号,通常用于连接显示器或 TV 等设备。如 HDMI、DisplayPort、DVI 等。每个 CRTC 可以有一个或多个 Encoder。
CONNECTOR 通常是用于将 Encoder 输出的信号传递给显示器,并与显示器建立连接。每个 Encoder 可以有一个或多个 Connector。
PLANE 硬件图层,负责获取显存,再输出到CRTC里,可以看作是一个显示器的图层,每个 crtc 至少要有一个 plane。通常会有多个 plane,每个 plane 可以分别设置自己的属性,从而实现多个图像内容的叠加。例如,在一个视频播放应用中,可以使用一个 plane 显示视频内容,另一个 plane 显示控制面板或字幕等内容,从而实现多个图像内容的同时显示。
Framebuffer 帧缓存,用于存储屏幕上的每个像素点的颜色信息。只用于描述显存信息(如 format、pitch、size 等),不负责显存的分配释放
VBLANK 软件和硬件的同步机制,RGB时序中的垂直消影区,软件通常使用硬件VSYNC来实现
property 原子操作的基础,任何你想设置的参数,都可以做成property,供用户空间使用,是DRM驱动中最灵活、最方便的Mode setting机制
DUMB 是一个dumb缓冲区,负责一些简单的buffer显示,可以通过CPU直接渲染
PRIME 连续、非连续物理内存都支持,基于DMA-BUF机制,可以实现buffer共享,多用于大内存复杂场景
fence buffer同步机制,基于内核dma_fence机制实现,用于防止显示内容出现异步问题

以上内容来自:参考内容1&2。

2、DRM内部Objects

上图蓝色部分则是对物理硬件的抽象,黄色部分则是对软件的抽象。虚线以上的为drm_mode_object,虚线以下为 drm_gem_object。

上面这些objects的关系如下图所示:

通过上图可以看到,plane 是连接 framebuffer 和 crtc 的纽带,而 encoder 则是连接 crtc 和 connector 的纽带。*与物理 buffer 直接打交道的是 gem 而不是 framebuffer。*

个人理解:

buffer是硬件存储设备, 由gem分配和释放,framebuffer用于描述分配的显存的信息(如 format、pitch、size 等),而plane用于描述图层信息,描述的是framebuffer中哪些点处于同一个图层,多个plane隶属于同一个crtc,crtc控制显卡输出图像信号,encoder将 crtc 输出的图像信号转换成一定格式的数字信号,如 HDMI、DisplayPort、DVI 等, connector用于将 Encoder 输出的信号传递给显示器,并与显示器建立连接。

需要注意的是,上图蓝色部分即使没有实际的硬件与之对应,在软件驱动中也需要实现这些 objects,否则 DRM 子系统无法正常运行。

3、drm_panel

Encoder 驱动程序负责将图形数据转换为 LCD 显示器所需的视频信号,而 Connector 驱动程序则负责将这些信号发送到正确的显示设备上。LCD 驱动程序需要和Encoder、Connector 这两个驱动程序进行交互,以完成图形输出的控制。

耦合的产生:
(1)connector 的主要作用就是获取显示参数,所以会在 LCD 驱动中去构造 connector object。但是 connector 初始化时需要 attach 上一个 encoder object,而这个 encoder object 往往是在另一个硬件驱动中生成的,为了访问该 encoder object,势必会产生一部分耦合的代码。
(2)encoder 除了扮演信号转换的角色,还担任着通知显示设备休眠唤醒的角色。因此,当 encoder 通知 LCD 驱动执行相应的 enable/disable 操作时,就一定会调用 LCD 驱动导出的全局函数,这也必然会产生一部分的耦合代码。

Encoder 驱动与 LCD 驱动之间的耦合会造成以下问题:

  1. 可维护性下降:耦合代码过多,使得维护代码变得困难。
  2. 可移植性下降:由于硬件平台的不同,encoder 驱动的实现可能不同,这就导致了 LCD 驱动对 encoder 驱动的依赖程度变高,降低了代码的可移植性。
  3. 系统可扩展性下降:由于 encoder 驱动与 LCD 驱动的紧耦合关系,当系统需要支持新的显示设备时,需要修改大量的代码,增加了系统扩展性的难度。

为了解决该耦合的问题,DRM 子系统为开发人员提供了 drm_panel 结构体,该结构体封装了 connector & encoder 对 LCD 访问的常用接口。

于是,原来的 Encoder 驱动和 LCD 驱动之间的耦合,就转变成了上图中 Encoder 驱动与 drm_panel、drm_panel 与 LCD 驱动之间的“耦合”,从而实现了 Encoder 驱动与 LCD 驱动之间的解耦合。

drm_panel 不属于 objects 的范畴,它只是一堆回调函数的集合。但它的存在降低了 LCD 驱动与 encoder 驱动之间的耦合度。


三、DRM 驱动常用结构体

🍚 *drm_mode_object*

对于plane、crtc、encoder、connector 几个对象,DRM框架将其称作“对象”,有一个公共基类struct drm_mode_object,这几个对象都由此基类扩展而来(该类作为crtc等结构体的成员)。

如 struct crtc 结构体中存在****基类成员 struct drm_mode_object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* struct drm_crtc - central CRTC control structure
*
* Each CRTC may have one or more connectors associated with it. This structure
* allows the CRTC to be controlled.
*/
struct drm_crtc {
/*挂载到&drm_mode_config.crtc_list*/
struct list_head head;
/** crtc名称 */
char *name;
/** kms mode对象 */
struct drm_mode_object base;// 基类
/*primary层*/
struct drm_plane *primary;
/*鼠标层*/
struct drm_plane *cursor;
/*序号*/
unsigned index;
...

};

事实上,这个基类扩展出来的子类并不是只有上面提到的几种,本文只考虑以上四种。以下为结构体 drm_mode_object 的定义:

1
2
3
4
5
6
7
struct drm_mode_object {
uint32_t id;
uint32_t type;
struct drm_object_properties *properties;
struct kref refcount;
void (*free_cb)(struct kref *kref);
};

struct drm_mode_object 中 id 和 type 分别为这个对象在KMS子系统中的ID和类型(即上面提到的plane、CRTC等)。drm_mode_object 从的定义中即可发现其实现了两个比较重要的功能:

kref 引用计数及生命周期管理:

指 drm_mode_object 对象在内核中的生命周期的管理。每个 drm_mode_object 对象都有一个引用计数,当一个对象被创建时,它的引用计数被初始化为1。每当一个新的引用指向该对象时,它的引用计数就会增加1,每当一个引用被释放时,它的引用计数就会减少1。当对象的引用计数降为0时,内核会自动释放该对象。这种方式确保了内核中不会存在不再使用的对象,从而避免了内存泄漏。

drm_object_properties 属性管理:

属性管理是指在DRM驱动中,每个对象都可以拥有一组属性(例如分辨率、刷新率等),并且可以动态地增加、删除或修改属性。这些属性可以被用户空间的应用程序或者其他驱动程序获取或者设置。

🍚 Framebuffer

注意framebuffer没有对应的回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// include/drm/drm_framebuffer.h
struct drm_framebuffer {
struct drm_device *dev;
struct list_head head;
struct drm_mode_object base;
const struct drm_format_info *format; // drm格式信息
const struct drm_framebuffer_funcs *funcs;
unsigned int pitches[4]; // Line stride per buffer
unsigned int offsets[4]; // Offset from buffer start to the actual pixel data in bytes, per buffer.
uint64_t modifier; // Data layout modifier
unsigned int width;
unsigned int height;
int flags;
int hot_x;
int hot_y;
struct list_head filp_head;
struct drm_gem_object *obj[4];
};

struct drm_framebuffer 主要元素的展示如下图所示(来自brezillon-drm-kms),内存缓冲区组织,采取FOURCC格式代码:

🍚 Plane

drm_plane结构体详见:LXR linux/include/drm/drm_plane.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct drm_plane_funcs 结构体成员:
1、 update_plane:为给定的CRTC和framebuffer启用并配置平面
2、 disable_plane:关闭plane
3、 destroy:清除plane所有的资源
4、 reset:复位软硬件状态为关闭
5、 set_property:更新plane的property
6、 atomic_duplicate_state:复制当前的plane状态并返回
7、 atomic_destroy_state:销毁当前的plane状态
8、 atomic_set/get_property:原子操作设置/获得property
9、 format_mod_supported:该可选挂钩用于DRM,以确定给定的格式/修饰符组合是否对平面有效
struct drm_plane_helper_funcs结构体成员:
1、 prepare_fb:该钩子通过固定其后备存储器或将其重新定位到连续的VRAM块,为扫描准备帧缓冲区。其他可能的准备工作包括冲洗缓存。
2、 cleanup_fb:清除在prepare_fb分配的给framebuffer和plane的资源
3、 atomic_check:检查该挂钩中的plane特定约束
4、 atomic_update:更新plane状态
5、 atomic_disable:关闭
6、 atomic_async_check:异步检查
7、 atomic_async_update:异步更新状态

🍚 Crtc

drm_crtc结构体详见:LXR linux/include/drm/drm_crtc.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct drm_crtc_funcs结构体成员:
1、 reset;复位,设置为关闭状态
2、 destroy:清除CRTC资源
3、 cursor_set:更新鼠标图像
4、 cursor_move:更新光标位置
5、 gamma_set:gamma设置
6、 set_config:修改配置
7、 page_flip:修改帧缓冲页,避免垂直消影期间用新的缓冲区替代时产生撕裂
8、 set_property:设置property
9、 atomic_duplicate_state:复制当前的状态
10、 atomic_destroy_state:销毁复制的状态
11、 atomic_get_property:获取atomic_get_property
12、 set_crc_source:更改CRC的来源
13、 get_vblank_counter 获取CRTC的vblank计数器
14、 disable_vblank:关闭消影
struct drm_crtc_helper_funcs结构体成员
1、 dpms:控制CRTC电源功率
2、 commit:提交新的模式
3、 mode_valid:用于检查特定模式在此crtc中是否有效
4、 mode_fixup:用于验证模式
5、 mode_set:设置模式
6、 mode_set_nofb:用于更新CRTC的显示模式,而不更改主平面配置的任何内容
7、 mode_set_base:设置新的帧缓冲区和扫描位置
8、 atomic_flush:完成CRTC上多个平面的更新
9、 atomic_enable/disable:打开/关闭

🍚 Encoder

drm_encoder结构体详见:LXR linux/include/drm/drm_encoder.h

1
2
3
4
5
6
7
8
static const struct drm_encoder_funcs ltdc_encoder_funcs = {
.destroy = drm_encoder_cleanup,
};
static const struct drm_encoder_helper_funcs ltdc_encoder_helper_funcs = {
.disable = ltdc_encoder_disable,
.enable = ltdc_encoder_enable,
.mode_set = ltdc_encoder_mode_set,
};

🍚 helper 函数与使用

很多文章对 helper 函数和 drm_xxx_helper_funcs 函数产生了混淆!!!以下是我的理解,有错误评论区指出。

  • drm_xxx_funcs 和 drm_xxx_helper_funcs 结构体是用于表示某个组件的基本操作和辅助操作的结构体;
  • 而 helper 函数是 DRM 框架提供的通用函数,用于实现 DRM 驱动程序中常见的操作。

在 Linux DRM 驱动中,helper 函数指的是由 DRM 框架提供的一组辅助函数,用于简化驱动程序的实现。这些函数包括了多种类型的操作,如模式设置、原子操作、缓冲区管理等。

常见的helper函数:

在 drm_xxx_funcs 可以自定义回调函数(完全不使用helper函数),或者使用DRM驱动提供的helper函数直接填充(使用helper函数),如果直接使用helper函数填充不足以满足要求,还可以使用 drm_xxx_helper_funcs 中的回调函数进行补充、修改、覆盖****!

终于理解了!如果你觉着有用,点个赞鼓励一下吧!

🍚 drm_xxx_funcs 和 drm_xxx_helper_funcs 区别

****在DRM子系统中,当需要执行特定组件的操作时,就会调用对应的回调函数,这些回调函数会根据其实现的功能,对特定组件进行相应的操作。****比如当系统检测到连接状态发生变化时,会调用drm_connector_funcs中的detect函数,该函数会根据当前连接状态,更新drm_connector的状态,并将状态通知给上层应用程序和显示管道。

drm_xxx_funcs 和 drm_xxx_helper_funcs 都是用来定义回调函数的结构体,它们之间的区别在于它们包含的函数指针的类型和含义不同。

它们的区别在于,drm_xxx_funcs 中包含一些必要的、基本的由驱动程序实现的函数指针,而 drm_xxx_helper_funcs 中包含一些可选的、辅助的由驱动程序选择性实现的辅助函数指针。

总结(来自参考文献,并进行修改):

drm_xxx_funcs 是 drm ioctl 操作的最终入口,但是对于大多数 SoC 厂商来说,它们的 drm_xxx_funcs 操作流程基本相同(你抄我,我抄你),只是在寄存器配置上存在差异,因此开发者们将那些 common 的操作流程抽象成了 helper 库函数,而将那些厂商差异化的代码放到了 drm_xxx_helper_funcs 中去,用于补充和客制化 helper 库函数。

架构优势:

这样双层的实现即能保证开发者有足够高的自由度(完全不用helper函数),也能简化开发者的开发(使用helper函数),同时提供给开发者hook(修改或者覆盖)特定helper函数的能力。


四、DRM驱动框架(VKMS)

1、VKMS 简介

VKMS 是 “Virtual Kernel Mode Setting” 的缩写,它于2018年7月5日被合入到 linux-4.19 主线版本中,并存放在 drivers/gpu/drm/vkms 目录下。之所以称它为 Virtual KMS,是因为该驱动不需要真实的硬件,它完全是一个软件虚拟的“显示”设备,甚至连显示都算不上,因为当它运行时,你看不到任何显示内容。它唯一能提供的,就是一个由高精度 timer 模拟的 VSYNC 中断信号!

该驱动存在的目的,主要是为了 DRM 框架自测试,以及方便那些无头显示器设备的调试应用。虽然我们看不到 VKMS 的显示效果,但是在驱动流程上,它实现了 modesetting 该有的基本操作(Modesetting是指在显示器上渲染图像的过程中,调整显示器的分辨率、刷新率、像素格式等参数,以适应不同的应用场景和显示设备。)。因其逻辑简单,代码量少,拿来做学习案例讲解再好不过。

**VKMS在Linux源码下的分布**

随着内核版本的不断升级,添加到 VKMS 的功能也越来越多,截止到内核版本 kernel 5.7-rc2,该 VKMS 驱动已经集成了如下功能:

  • Atomic Modeset
  • VBlank
  • Dumb Buffer
  • Cursor & Primary Plane
  • Framebuffer CRC 校验
  • Plane Composition
  • GEM Prime Import

下面我们将一步一步学习如何从零开始撰写一个VKMS驱动。

2、最简单的VKMS驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <drm/drmP.h>

static struct drm_device drm;// 用于抽象一个完整的DRM设备

static struct drm_driver vkms_driver = {
.name = "vkms",
.desc = "Virtual Kernel Mode Setting",
.date = "20180514",
.major = 1,
.minor = 0,
};

static int __init vkms_init(void)
{
drm_dev_init(&drm, &vkms_driver, NULL);
drm_dev_register(&drm, 0);

return 0;
}

module_init(vkms_init);

我们将该驱动以 build-in 方式编译进内核,然后启动内核,如果你在 kernel log 中仔细查找,会发现有如下 drm log, 这些信息正是从上面的 name、date、major、minor 字段中获取的。

1
[drm] Initialized vkms 1.0.0 20180514 for virtual device on minor 0

除此之外,DRM 框架还为我们做了下面这些事情:

创建设备节点:/dev/dri/card0

创建 sysfs 节点:/sys/class/drm/card0

创建 debugfs 节点:/sys/kernel/debug/dri/0

当然,简单是以牺牲功能为代价的。该驱动目前什么事情也做不了,你唯一能做的就是查看该驱动的名字:

1
2
$ cat /sys/kernel/debug/dri/0/name
vkms unique=vkms

3、VKMS驱动添加 fops 操作接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <drm/drmP.h>

static struct drm_device drm;// 用于抽象一个完整的DRM设备

static const struct file_operations vkms_fops = {
.owner = THIS_MODULE,
.open = drm_open,
.release = drm_release,
.unlocked_ioctl = drm_ioctl,
.poll = drm_poll,
.read = drm_read,
};

static struct drm_driver vkms_driver = {
.fops = &vkms_fops,

.name = "vkms",
.desc = "Virtual Kernel Mode Setting",
.date = "20180514",
.major = 1,
.minor = 0,
};

static int __init vkms_init(void)
{
drm_dev_init(&drm, &vkms_driver, NULL);
drm_dev_register(&drm, 0);

return 0;
}

module_init(vkms_init);

有了 fops,我们就可以对 card0 进行 open / read 操作了。更重要的是,我们现在可以进行一些简单的 ioctl 操作了(DRM相关只读的操作,且不需要调用DRM相关回调函数的操作)!但是,到目前为止,凡是和 modesetting 相关的操作,我们还是操作不了。

4、添加 drm mode objects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <drm/drmP.h>
#include <drm/drm_encoder.h>

static struct drm_device drm;// 用于抽象一个完整的DRM设备
static struct drm_plane primary;
static struct drm_crtc crtc;
static struct drm_encoder encoder;
static struct drm_connector connector;

static const struct drm_plane_funcs vkms_plane_funcs;
static const struct drm_crtc_funcs vkms_crtc_funcs;
static const struct drm_encoder_funcs vkms_encoder_funcs;
static const struct drm_connector_funcs vkms_connector_funcs;

static const u32 vkms_formats[] = {
DRM_FORMAT_XRGB8888,
};

static void vkms_modeset_init(void)
{
drm_mode_config_init(&drm);// 初始化KMS框架(初始化一些全局结构体),本质上是初始化drm_device中mode_config结构体

// 初始化drm_device中包含的drm_connector,drm_crtc等对象
drm_universal_plane_init(&drm, &primary, 0, &vkms_plane_funcs,
vkms_formats, ARRAY_SIZE(vkms_formats),
NULL, DRM_PLANE_TYPE_PRIMARY, NULL);

drm_crtc_init_with_planes(&drm, &crtc, &primary, NULL, &vkms_crtc_funcs, NULL);

drm_encoder_init(&drm, &encoder, &vkms_encoder_funcs, DRM_MODE_ENCODER_VIRTUAL, NULL);

drm_connector_init(&drm, &connector, &vkms_connector_funcs, DRM_MODE_CONNECTOR_VIRTUAL);
}

static const struct file_operations vkms_fops = {
.owner = THIS_MODULE,
.open = drm_open,
.release = drm_release,
.unlocked_ioctl = drm_ioctl,
.poll = drm_poll,
.read = drm_read,
};

static struct drm_driver vkms_driver = {
.driver_features = DRIVER_MODESET, // 添加上 DRIVER_MODESET 标志位,告诉 DRM Core 当前驱动支持 modesetting 操作
.fops = &vkms_fops,

.name = "vkms",
.desc = "Virtual Kernel Mode Setting",
.date = "20180514",
.major = 1,
.minor = 0,
};

static int __init vkms_init(void)
{
drm_dev_init(&drm, &vkms_driver, NULL);

vkms_modeset_init();

drm_dev_register(&drm, 0);

return 0;
}

module_init(vkms_init);

由于上面4个 objects 在创建时,它们的 callback funcs 没有赋初值,所以真正的 modeset 操作目前还无法正常执行,不过我们至少可以使用一些只读的 modeset IOCTL 了(DRM objects 相关只读的操作,且不需要调用DRM相关回调函数的操作)。

5、添加 FB 和 GEM 支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <drm/drmP.h>
#include <drm/drm_encoder.h>
#include <drm/drm_fb_cma_helper.h>
#include <drm/drm_gem_cma_helper.h>

static struct drm_device drm;// 用于抽象一个完整的DRM设备
static struct drm_plane primary;
static struct drm_crtc crtc;
static struct drm_encoder encoder;
static struct drm_connector connector;

static const struct drm_plane_funcs vkms_plane_funcs;
static const struct drm_crtc_funcs vkms_crtc_funcs;
static const struct drm_encoder_funcs vkms_encoder_funcs;
static const struct drm_connector_funcs vkms_connector_funcs;

/* add here */
// 定义了一组函数指针,用于管理驱动程序中的显示模式配置。
// 这些函数指针包括添加和删除连接器、CRTC和编解码器,以及更新显示模式等功能。
// 这些函数在驱动程序中被调用以进行显示模式的管理和配置。
static const struct drm_mode_config_funcs vkms_mode_funcs = {
.fb_create = drm_fb_cma_create, // 根据给定的帧缓冲参数,创建一个新的帧缓冲对象,并返回其句柄
};

static const u32 vkms_formats[] = {
DRM_FORMAT_XRGB8888,
};

static void vkms_modeset_init(void)
{
drm_mode_config_init(&drm);// 初始化KMS框架,本质上是初始化drm_device中的mode_config结构体
// 填充mode_config中int min_width, min_height; int max_width, max_height的值,这些值是framebuffer的大小限制
drm.mode_config.max_width = 8192;
drm.mode_config.max_height = 8192;
/* add here */
// 设置mode_config->funcs指针,本质是一组由驱动实现的回调函数,涵盖KMS中一些相当基本的操作
drm.mode_config.funcs = &vkms_mode_funcs;

// 初始化drm_device中包含的drm_connector,drm_crtc等对象
drm_universal_plane_init(&drm, &primary, 0, &vkms_plane_funcs,
vkms_formats, ARRAY_SIZE(vkms_formats),
NULL, DRM_PLANE_TYPE_PRIMARY, NULL);

drm_crtc_init_with_planes(&drm, &crtc, &primary, NULL, &vkms_crtc_funcs, NULL);

drm_encoder_init(&drm, &encoder, &vkms_encoder_funcs, DRM_MODE_ENCODER_VIRTUAL, NULL);

drm_connector_init(&drm, &connector, &vkms_connector_funcs, DRM_MODE_CONNECTOR_VIRTUAL);
}

// 文件操作回调函数
static const struct file_operations vkms_fops = {
.owner = THIS_MODULE,
.open = drm_open,
.release = drm_release,
.unlocked_ioctl = drm_ioctl,
.poll = drm_poll,
.read = drm_read,
/* add here */
.mmap = drm_gem_cma_mmap, // DRM的GEM对象映射到用户空间,以便用户空间可以使用它
};

static struct drm_driver vkms_driver = {
.driver_features = DRIVER_MODESET | DRIVER_GEM, // 添加DRIVER_GEM 标志位,告诉 DRM Core 该驱动支持 GEM 操作
.fops = &vkms_fops,

/* add here */
.dumb_create = drm_gem_cma_dumb_create, //
.gem_vm_ops = &drm_gem_cma_vm_ops,
.gem_free_object_unlocked = drm_gem_cma_free_object,

.name = "vkms",
.desc = "Virtual Kernel Mode Setting",
.date = "20180514",
.major = 1,
.minor = 0,
};

static int __init vkms_init(void)
{
drm_dev_init(&drm, &vkms_driver, NULL);

vkms_modeset_init();

drm_dev_register(&drm, 0);

return 0;
}

module_init(vkms_init);

现在,我们可以使用 IOCTL 来进行一些标准的 GEM 和 FB 操作了!

驱动解析1 :

我们知道drm_device用于抽象一个完整的DRM设备,而其中与Mode Setting相关的部分则由drm_mode_config 进行管理。drm_mode_config 的主要功能之一是提供对显示器模式的管理和配置。这包括添加、删除、修改和查询显示器模式的能力。此外,drm_mode_config还提供了与模式相关的配置选项,例如色彩空间、刷新率、分辨率等等。

在 drm_device 中存在 mode_config 这个 drm_mode_config 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* struct drm_device - DRM device structure
*
* This structure represent a complete card that
* may contain multiple heads.
*/
struct drm_device {
/** @if_version: Highest interface version set */
int if_version;

/** @ref: Object ref-count */
struct kref ref;

...

/** @mode_config: Current mode config */
struct drm_mode_config mode_config;

...
};

在 mode_config 这个 *drm_mode_config 结构体中存在一个*类*型为 drm_mode_config_func 的回调函数 func。*drm_mode_config_func是一个函数指针结构体,用于驱动程序向内核注册显示器模式配置的回调函数。**这些函数指针包括添加和删除连接器、CRTC和编解码器,以及更新显示模式等功能。当内核需要对显示器模式进行配置或管理时,它将调用这些回调函数以执行相应操作。

drm_mode_config_funcs结构体中的 fb_create 作用:

fb_create 函数的作用是根据给定的帧缓冲参数,创建一个新的**帧缓冲对象**(并不是分配内存,只是创建帧缓冲对象,因为 framebuffer 不涉及内存的分配与释放)****,并返回其句柄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* struct drm_mode_config - Mode configuration control structure
* @min_width: minimum fb pixel width on this device
* @min_height: minimum fb pixel height on this device
* @max_width: maximum fb pixel width on this device
* @max_height: maximum fb pixel height on this device
* @funcs: core driver provided mode setting functions
* @fb_base: base address of the framebuffer
* @poll_enabled: track polling support for this device
* @poll_running: track polling status for this device
* @delayed_event: track delayed poll uevent deliver for this device
* @output_poll_work: delayed work for polling in process context
* @preferred_depth: preferred RBG pixel depth, used by fb helpers
* @prefer_shadow: hint to userspace to prefer shadow-fb rendering
* @cursor_width: hint to userspace for max cursor width
* @cursor_height: hint to userspace for max cursor height
* @helper_private: mid-layer private data
*
* Core mode resource tracking structure. All CRTC, encoders, and connectors
* enumerated by the driver are added here, as are global properties. Some
* global restrictions are also here, e.g. dimension restrictions.
*/
struct drm_mode_config {
...

int min_width, min_height;
int max_width, max_height;
const struct drm_mode_config_funcs *funcs;
resource_size_t fb_base;

...
};

驱动解析2 :

**dumb_create: struct drm_driver 结构体中的一个函数指针,它指向的函数用于在 DRM 子系统中**创建 dumb buffer,也就是一个简单的、未映射的内存区域,**通常用于测试或临时存储。**

dumb_create:分配 dumb buffer 的回调接口,主要完成三件事:
(1)创建 gem object
(2)创建 gem handle
(3)分配物理 buffer (也可以等到后面再分配)
本例中直接使用 CMA helper 函数实现,该函数内部会分配最终的物理 buffer。

GEM分配的内存区域通过映射之后交给Frame buffer驱动程序使用。*Frame buffer驱动程序不直接分配内存,而是通过访问GEM对象来获取内存区域的物理地址等信息,以便正确地管理和利用帧缓冲区中的显示数据。*

在将GEM映射到帧缓冲区时,需要经过以下两个步骤:

  1. 将GEM对象中的内存区域映射到内核空间的虚拟地址空间中。
  2. 将内核空间中的GEM内存区域映射到显示设备的显存中。

驱动解析3 :

mmp: 将GEM内存映射到用户控件。创建 dumb buffer 的目的就是要拿去给 CPU 画图,因此没有 mmap 的 dumb buffer 是没有灵魂的,所以必须实现。通常使用 drm_gem_mmap() 来实现。在 kernel 驱动中,实现 mmap 系统调用离不开两个关键步骤:(1)内存分配 (2) 建立映射关系。这刚好也对应了 DRM 中的 dumb_createmmap 操作。

驱动解析4 :

gem_vm_ops:该函数指针中包含的各种操作函数,例如 fault, open, close, access 等,都是用于实现 GEM 内存管理机制的各种功能。

  • mmap 函数是将设备内存映射到进程的用户空间,让应用程序可以直接访问设备内存。
  • gem_vm_ops 函数指针则是在用户空间程序请求显存时被调用(mmp),用于控制显存的访问权限、分配显存、映射显存到用户空间等操作,从而确保显存的安全性和高效性。

这个过程涉及到分配一块虚拟内存、映射物理内存、建立页表等操作。 因此,在实现 DRM 驱动时,这两个函数通常需要一起实现,以支持对设备内存的映射操作。

驱动解析5 :

gem_free_object_unlocked :该指针指向一个函数,用于释放GEM对象的资源。具体来说,当一个GEM对象不再被使用时,该函数将被调用来释放该对象的内存、锁定的页面等资源。这个函数指针可以在结构体中 struct drm_driver 定义,作为驱动的一部分,因此在驱动代码中可以使用它来释放GEM对象的资源。

6、实现 callback funcs 并添加 Legacy Modeset 支持

Legacy Modeset 是指在 Linux DRM 框架中使用传统的 KMS 模式设置,也称为“旧模式设置”。

在 Legacy Modeset 中,驱动程序需要执行多个步骤来更改图形模式,例如分配帧缓冲,设置分辨率和刷新率等。这些步骤是分开执行的,如果其中的任何一步出现错误,整个操作都可能失败。可能会导致画面闪烁、撕裂或丢失帧等问题,而且驱动程序必须花费额外的时间来撤消之前的更改。

因此,Legacy Modeset 的主要缺点是不具备原子性,无法保证更改的一致性和可靠性。这也是 Atom Modeset 出现的主要原因。Atom Modeset 可以将多个操作封装成一个事务并原子地提交给硬件,从而保证所有更改都是一致和可靠的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
#include <drm/drm_crtc_helper.h>
#include <drm/drm_plane_helper.h>
#include <drm/drm_fb_cma_helper.h>
#include <drm/drm_gem_cma_helper.h>

static struct drm_device drm;
static struct drm_plane primary;
static struct drm_crtc crtc;
static struct drm_encoder encoder;
static struct drm_connector connector;

static void vkms_crtc_dpms(struct drm_crtc *crtc, int mode)
{
}

static int vkms_crtc_mode_set(struct drm_crtc *crtc,
struct drm_display_mode *mode,
struct drm_display_mode *adjusted_mode,
int x, int y, struct drm_framebuffer *old_fb)
{
return 0;
}

static void vkms_crtc_prepare(struct drm_crtc *crtc)
{
}

static void vkms_crtc_commit(struct drm_crtc *crtc)
{
}

static int vkms_crtc_page_flip(struct drm_crtc *crtc,
struct drm_framebuffer *fb,
struct drm_pending_vblank_event *event,
uint32_t page_flip_flags,
struct drm_modeset_acquire_ctx *ctx)
{
unsigned long flags;

crtc->primary->fb = fb;
if (event) {
spin_lock_irqsave(&crtc->dev->event_lock, flags);
drm_crtc_send_vblank_event(crtc, event);
spin_unlock_irqrestore(&crtc->dev->event_lock, flags);
}
return 0;
}

static const struct drm_crtc_helper_funcs vkms_crtc_helper_funcs = {
.dpms = vkms_crtc_dpms,
.mode_set = vkms_crtc_mode_set,
.prepare = vkms_crtc_prepare,
.commit = vkms_crtc_commit,
};

static const struct drm_crtc_funcs vkms_crtc_funcs = {
.set_config = drm_crtc_helper_set_config,
.page_flip = vkms_crtc_page_flip,
.destroy = drm_crtc_cleanup,
};

static const struct drm_plane_funcs vkms_plane_funcs = {
.update_plane = drm_primary_helper_update,
.disable_plane = drm_primary_helper_disable,
.destroy = drm_plane_cleanup,
};

static int vkms_connector_get_modes(struct drm_connector *connector)
{
int count;

count = drm_add_modes_noedid(connector, 8192, 8192);
drm_set_preferred_mode(connector, 1024, 768);

return count;
}

static struct drm_encoder *vkms_connector_best_encoder(struct drm_connector *connector)
{
return &encoder;
}

static const struct drm_connector_helper_funcs vkms_conn_helper_funcs = {
.get_modes = vkms_connector_get_modes,
.best_encoder = vkms_connector_best_encoder,
};


static const struct drm_connector_funcs vkms_connector_funcs = {
.dpms = drm_helper_connector_dpms,
.fill_modes = drm_helper_probe_single_connector_modes,
.destroy = drm_connector_cleanup,
};

static const struct drm_encoder_funcs vkms_encoder_funcs = {
.destroy = drm_encoder_cleanup,
};

static const struct drm_mode_config_funcs vkms_mode_funcs = {
.fb_create = drm_fb_cma_create,
};

static const u32 vkms_formats[] = {
DRM_FORMAT_XRGB8888,
};

static void vkms_modeset_init(void)
{
drm_mode_config_init(&drm);
drm.mode_config.max_width = 8192;
drm.mode_config.max_height = 8192;
drm.mode_config.funcs = &vkms_mode_funcs;

drm_universal_plane_init(&drm, &primary, 0, &vkms_plane_funcs,
vkms_formats, ARRAY_SIZE(vkms_formats),
NULL, DRM_PLANE_TYPE_PRIMARY, NULL);

drm_crtc_init_with_planes(&drm, &crtc, &primary, NULL, &vkms_crtc_funcs, NULL);
drm_crtc_helper_add(&crtc, &vkms_crtc_helper_funcs);

drm_encoder_init(&drm, &encoder, &vkms_encoder_funcs, DRM_MODE_ENCODER_VIRTUAL, NULL);

drm_connector_init(&drm, &connector, &vkms_connector_funcs, DRM_MODE_CONNECTOR_VIRTUAL);
drm_connector_helper_add(&connector, &vkms_conn_helper_funcs);
drm_mode_connector_attach_encoder(&connector, &encoder);
}

static const struct file_operations vkms_fops = {
.owner = THIS_MODULE,
.open = drm_open,
.release = drm_release,
.unlocked_ioctl = drm_ioctl,
.poll = drm_poll,
.read = drm_read,
.mmap = drm_gem_cma_mmap,
};

static struct drm_driver vkms_driver = {
.driver_features = DRIVER_MODESET | DRIVER_GEM,
.fops = &vkms_fops,

.dumb_create = drm_gem_cma_dumb_create,
.gem_vm_ops = &drm_gem_cma_vm_ops,
.gem_free_object_unlocked = drm_gem_cma_free_object,

.name = "vkms",
.desc = "Virtual Kernel Mode Setting",
.date = "20180514",
.major = 1,
.minor = 0,
};

static int __init vkms_init(void)
{
drm_dev_init(&drm, &vkms_driver, NULL);

vkms_modeset_init();

drm_dev_register(&drm, 0);

return 0;
}

module_init(vkms_init);
  1. *xxx_funcs* 必须有,*xxx_helper_funcs* 可以没有。
  2. drm_xxx_init() 必须有,drm_xxx_helper_add() 可以没有。
  3. 只有当 xxx_funcs 采用 DRM 标准的 helper 函数实现时,才有可能 需要定义 xxx_helper_funcs 接口。
  4. xxx_funcs.destroy() 接口必须实现。

有了各种 funcs 和 helper funcs,我们现在终于可以执行真正的 modeset 操作了,不过目前只支持 legacy modeset。

7、将 Legacy code 转换为 Atomic 版本

DRM驱动的legacy模式是一种传统的方法,它使用一个内核模块来管理DRM资源,支持在内核空间和用户空间之间进行资源管理。这种模式比较简单,但性能不太好,而且容易出现错误。

Atomic模式是一种更新的模式,它使用多个内核模块来管理DRM资源,并且支持在内核空间和用户空间之间进行原子操作,从而提供更好的性能和可靠性。Atomic模式比legacy模式更复杂,但性能更好,而且更加可靠。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#include <drm/drm_atomic_helper.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_fb_cma_helper.h>
#include <drm/drm_gem_cma_helper.h>
#include <linux/hrtimer.h>

static struct drm_device drm;
static struct drm_plane primary;
static struct drm_crtc crtc;
static struct drm_encoder encoder;
static struct drm_connector connector;
static struct hrtimer vblank_hrtimer;

static enum hrtimer_restart vkms_vblank_simulate(struct hrtimer *timer)
{
drm_crtc_handle_vblank(&crtc);

hrtimer_forward_now(&vblank_hrtimer, 16666667);

return HRTIMER_RESTART;
}

static void vkms_crtc_atomic_enable(struct drm_crtc *crtc,
struct drm_crtc_state *old_state)
{
hrtimer_init(&vblank_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
vblank_hrtimer.function = &vkms_vblank_simulate;
hrtimer_start(&vblank_hrtimer, 16666667, HRTIMER_MODE_REL);
}

static void vkms_crtc_atomic_disable(struct drm_crtc *crtc,
struct drm_crtc_state *old_state)
{
hrtimer_cancel(&vblank_hrtimer);
}

static void vkms_crtc_atomic_flush(struct drm_crtc *crtc,
struct drm_crtc_state *old_crtc_state)
{
unsigned long flags;

if (crtc->state->event) {
spin_lock_irqsave(&crtc->dev->event_lock, flags);
drm_crtc_send_vblank_event(crtc, crtc->state->event);
spin_unlock_irqrestore(&crtc->dev->event_lock, flags);

crtc->state->event = NULL;
}
}

static const struct drm_crtc_helper_funcs vkms_crtc_helper_funcs = {
.atomic_enable = vkms_crtc_atomic_enable,
.atomic_disable = vkms_crtc_atomic_disable,
.atomic_flush = vkms_crtc_atomic_flush,
};

static const struct drm_crtc_funcs vkms_crtc_funcs = {
.set_config = drm_atomic_helper_set_config,
.page_flip = drm_atomic_helper_page_flip,
.destroy = drm_crtc_cleanup,
.reset = drm_atomic_helper_crtc_reset,
.atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_crtc_destroy_state,
};

static void vkms_plane_atomic_update(struct drm_plane *plane,
struct drm_plane_state *old_state)
{
}

static const struct drm_plane_helper_funcs vkms_plane_helper_funcs = {
.atomic_update = vkms_plane_atomic_update,
};

static const struct drm_plane_funcs vkms_plane_funcs = {
.update_plane = drm_atomic_helper_update_plane,
.disable_plane = drm_atomic_helper_disable_plane,
.destroy = drm_plane_cleanup,
.reset = drm_atomic_helper_plane_reset,
.atomic_duplicate_state = drm_atomic_helper_plane_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_plane_destroy_state,
};

static int vkms_conn_get_modes(struct drm_connector *connector)
{
int count;

count = drm_add_modes_noedid(connector, 8192, 8192);
drm_set_preferred_mode(connector, 1024, 768);

return count;
}

static const struct drm_connector_helper_funcs vkms_conn_helper_funcs = {
.get_modes = vkms_conn_get_modes,
};

static const struct drm_connector_funcs vkms_connector_funcs = {
.fill_modes = drm_helper_probe_single_connector_modes,
.destroy = drm_connector_cleanup,
.reset = drm_atomic_helper_connector_reset, // reset函数通常被用于在DRM模式切换或重置时,重置plane对象的状态。
.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
};


// 在DRM中,encoder对象的状态更新和禁用等操作都是通过对应的CRTC对象的状态进行修改来实现的,因此在encoder_funcs结构体中确实不需要实现.atomic_duplicate_state成员。encoder对象的状态更新和禁用等操作都是由CRTC对象的状态更新和禁用等操作来完成的。
static const struct drm_encoder_funcs vkms_encoder_funcs = {
.destroy = drm_encoder_cleanup, // DRM框架中的一个helper函数,用于清理encoder对象的资源。
};

static const struct drm_mode_config_funcs vkms_mode_funcs = {
.fb_create = drm_fb_cma_create,
.atomic_check = drm_atomic_helper_check, // 遍历原子状态中的各个平面状态信息,并进行一些检查,以确保新的状态是合法的,并且可以被应用到显卡硬件上。
.atomic_commit = drm_atomic_helper_commit, // 根据原子状态中的各个平面状态信息,计算出新的显卡状态,并将其应用到显卡硬件上,从而实现显示输出
};

static const u32 vkms_formats[] = {
DRM_FORMAT_XRGB8888,
};

static void vkms_modeset_init(void)
{
drm_mode_config_init(&drm);
drm.mode_config.max_width = 8192;
drm.mode_config.max_height = 8192;
drm.mode_config.funcs = &vkms_mode_funcs;

drm_universal_plane_init(&drm, &primary, 0, &vkms_plane_funcs,
vkms_formats, ARRAY_SIZE(vkms_formats),
NULL, DRM_PLANE_TYPE_PRIMARY, NULL);
drm_plane_helper_add(&primary, &vkms_plane_helper_funcs);

drm_crtc_init_with_planes(&drm, &crtc, &primary, NULL, &vkms_crtc_funcs, NULL);
drm_crtc_helper_add(&crtc, &vkms_crtc_helper_funcs);

drm_encoder_init(&drm, &encoder, &vkms_encoder_funcs, DRM_MODE_ENCODER_VIRTUAL, NULL);

drm_connector_init(&drm, &connector, &vkms_connector_funcs, DRM_MODE_CONNECTOR_VIRTUAL);
drm_connector_helper_add(&connector, &vkms_conn_helper_funcs);
drm_mode_connector_attach_encoder(&connector, &encoder);

drm_mode_config_reset(&drm);
}

static const struct file_operations vkms_fops = {
.owner = THIS_MODULE,
.open = drm_open,
.release = drm_release,
.unlocked_ioctl = drm_ioctl,
.poll = drm_poll,
.read = drm_read,
.mmap = drm_gem_cma_mmap,
};

static struct drm_driver vkms_driver = {
.driver_features = DRIVER_MODESET | DRIVER_GEM | DRIVER_ATOMIC, // 给 driver_features 添加上 DRIVER_ATOMIC 标志位,告诉 DRM Core 该驱动支持 Atomic 操作。
.fops = &vkms_fops,

.dumb_create = drm_gem_cma_dumb_create,
.gem_vm_ops = &drm_gem_cma_vm_ops,
.gem_free_object_unlocked = drm_gem_cma_free_object,

.name = "vkms",
.desc = "Virtual Kernel Mode Setting",
.date = "20180514",
.major = 1,
.minor = 0,
};

static int __init vkms_init(void)
{
drm_dev_init(&drm, &vkms_driver, NULL);

vkms_modeset_init();

drm_vblank_init(&drm, 1);

drm.irq_enabled = true;

drm_dev_register(&drm, 0);

return 0;
}

module_init(vkms_init);

驱动分析1:

atomic_duplicate_state:创建一个新的原子状态,并将原子状态中的所有对象(包括CRTC、connector、encoder等等)进行复制。

atomic_destroy_state:释放原子状态占用的所有资源,包括CRTC、connector、encoder等等。具体来说,该函数会遍历原子状态对象中的所有对象,并依次释放它们占用的资源。另外,该函数还会释放原子状态对象本身。

驱动分析2:

drm_mode_config_funcs.atomic_commit() 接口是 atomic 操作的主要入口函数,必须实现。这里直接使用 drm_atomic_helper_commit() 函数实现。

drm_atomic_helper_commit() 函数的作用是帮助应用程序提交一组关联的对象的更新请求,并保证它们以原子方式生效,从而保证图形显示的稳定性和一致性。

在DRM中,图形显示涉及多个对象,包括CRTC(控制器)、encoder(编码器)和connector(连接器)等。这些对象之间存在复杂的依赖关系,更新请求必须以原子方式提交,以确保它们同时生效,从而避免出现不一致的图形显示。

驱动分析3:

在 plane/crtc/encoder/connector objects 初始化完成之后,一定要调用 drm_mode_config_reset() 来动态创建各个 pipeline 的软件状态(即 drm_xxx_state)。

驱动分析4:

与 Legacy 相比,Atomic 的 xxx_funcs 必须 实现如下接口:

reset()

atomic_duplicate_state()

atomic_destroy_state()

它们主要用于维护 drm_xxx_state 数据结构,不能省略!

驱动分析5:

drm_plane_helper_funcs.atomic_update() 必须实现!(不太理解)

这个函数用于在DRM原子模式下更新plane对象的状态。如果一个plane对象没有实现.atomic_update函数,那么在使用DRM原子模式时,就无法正确地更新该plane对象的状态,从而导致显示异常或者错误。

驱动分析6:

Atomic 操作依赖 VSYNC 中断(即 VBLANK 事件),因此需要使用 hrtimer 来提供软件中断信号。在驱动初始化时调用 drm_vblank_init(),在 VSYNC 中断处理函数中调用 drm_handle_vblank()。


五、总结

要实现一个 DRM KMS 驱动,通常需要实现如下代码:

fops、drm_driver

dumb_create、fb_create、atomic_commit

drm_xxx_funcs、drm_xxx_helper_funcs

drm_xxx_init()、drm_xxx_helper_add()

drm_dev_init()、drm_dev_register()

但这都只是表象,核心仍然是上面介绍的7个 objects,一切都围绕着这几个 objects 展开:

  • 为了创建 crtc/plane/encoder/connector objects,需要调用 drm_xxx_init()。
  • 为了创建 framebuffer object,需要实现 fb_create() callback。
  • 为了创建 gem object,需要实现 dumb_create() callback。
  • 为了创建 property objects,需要调用 drm_mode_config_init()。
  • 为了让这些 objects 动起来,需要实现各种 funcs 和 helper funcs。
  • 为了支持 atomic 操作,需要实现 atomic_commit() callback。

六、panel-simple.c文件分析

panel_simple.c只是DRM驱动中用于与RGB屏幕交互的一种手段(相当于connector),它仅适用于简单的设备和场景。

在panel_simple.c中的DRM驱动生成的/dev/dri/card0设备节点同时也会自动生成一个/dev/fb0设备节点,用于向后兼容传统的Framebuffer接口。

但需要注意的是,这个Framebuffer接口只能使用一些基本的功能,如显存映射和控制台输出等,而不能使用DRM提供的高级功能。

对于全志的DRM架构如下图所示:


七、参考内容

1.DRM图形显示框架

2.DRM(Direct Rendering Manager)学习简介_何小龙csdn_何小龙的博客-CSDN博客

3.DRM (Direct Rendering Manager) 的基本概念 - 代码天地

4.DRM几个重要的结构体及panel开发_drm panel_紫川宁520的博客-CSDN博客

5.《DRM 专栏》| 彻底入门 DRM 驱动 - 腾讯云开发者社区-腾讯云

6.DRM框架分析(二):KMS

7.DRM GEM 驱动程序开发(dumb)_何小龙的博客-CSDN博客

参考内容

  1. 墨云uboot移植

  2. 稚辉君github项目;

  3. 正点原子《嵌入式Linux驱动开发指南》


相关链接(侵删)

  1. F1C200s 的 GPIO 编号规则
  2. 硬件制作–从零开始自制linux掌上电脑(F1C200S)
  3. uboot移植–从零开始自制linux掌上电脑(F1C200S)
  4. 内核移植–从零开始自制linux掌上电脑(F1C200S)
  5. 根文件系统–从零开始自制linux掌上电脑(F1C200S)
  6. Debian系统–从零开始自制linux掌上电脑(F1C200S)
  7. 正点原子4.3寸RGB屏幕驱动–从零开始自制linux掌上电脑(F1C200S)
  8. GT1151触摸屏驱动–从零开始自制linux掌上电脑(F1C200S)
  9. USB驱动–从零开始自制linux掌上电脑(F1C200S)
  10. 音频驱动–从零开始自制linux掌上电脑(F1C200S)
  11. WiFi驱动–从零开始自制linux掌上电脑(F1C200S)
  12. NES游戏移植–从零开始自制linux掌上电脑(F1C200S)
  13. 【项目原理】多点触摸屏驱动原理
  14. 项目原理】DRM驱动概念、组成、框架、源码分析

=================我是分割线=================

欢迎到公众号来唠嗑: