嵌入式linux mp157 开发
开发环境
mp157开发板在烧录系统之前,需要先烧写 TF-A ,该程序是写在ARM处理器里面的,处理器里面也有点内存,几M的大小吧,像M的单片机的运行程序就是直接烧写在处理器里面,烧写linux一般是烧写在emmc,sd卡,硬盘里。
TF-A的作用是保护系统的环节,系统启动后,先开始运行TF-A,检测系统以及硬件配置这些。
每个做arm芯片的厂家会提供一套TF-A源码,然后制作板子再根据厂家提供的源码进行修改,打补丁,使其兼容自己的板子。
正点原子mp157的板子烧写TF-A步骤:
烧写的文件
[tf-a-stm32mp157d-atk-serialboot.stm32] 用于将烧录工具和arm先连接
[tf-a-stm32mp157d-atk-trusted.stm32] tf-a的源码
[u-boot.stm32] 在该uboot系统下进行烧写tf-a源码
[flashlayout.tsv] 类似与目录,说明烧烤程序的具体位置,在烧录时将这个文件添加后,即可开始下载程序
烧写工具
1、打开[STM32CubeProgrammer]软件,将拨码开关开启USB启动,然后连接usb,将flashlayout.tsv导入后,点击下载,即可完成
linux系统移植
linux系统移植不写了 没意思。。。。
Linux驱动开发
Linux驱动开发思维
linux驱动开发,就是将linux需要控制的硬件设备写成驱动程序,便于应用开发的时候调用,应用开发就可以通过调用驱动里写好的对应函数去直接控制硬件,APP里面调用:C库里面的open函数,read函数,write函数,close函数。
引用正点原子介绍:
1、没有MKD,IAR这样的集成开发环境
2、linux下驱动开发模块化,复用化很强。专业术语就是驱动的分离与分层。
3、因为linux所支持的芯片,架构太多了,所以它需要考虑到为所有的芯片提供统一的驱动框架,Linux驱动的学习就是掌握各种驱动框架。
4、基本不与主控芯片的寄存器打交道。
5、在ubuntu下进行开发。
6、linux驱动开发和,应用开发是分开的。驱动做好以后给应用提供的就是一个“文件”,/dev/xx,
7、最新linux内核使用一个叫“设备树”文件,.dts,编译完就是.dtb。描述设备的树形的文件。
不废话了,万板起源:“点亮一个LED!”
驱动层
驱动模块的加载与卸载
Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启
动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在
Linux 内核启动以后使用“modprobe”或者“insmod”命令加载驱动模块。
加载模块函数:module_init(xxx_init); //注册模块加载函数
卸载模块函数:module_exit(xxx_exit); //注册模块卸载函数
在linux系统中,通过 modprobe
命令 加载驱动文件,rmmod
命令卸载驱动文件,也可以使用“modprobe -r”命令卸载驱动
使用modprobe命令加载新的驱动前,需要执行depmod
命令更新驱动依赖关系。
modprobe
命令 使用: modprobe xxx.ko
将会加载当前文件下 xxx.ko文件(xxx.ko文件是由xxx.c驱动程序通过编译内核的方式编译出来的文件,后面会讲) ,并且执行该程序内的入口函数module_init(xxx_init);
modprobe -r xxx.ko
或者rmmod xxx.ko
卸载驱动文件,执行该程序内的出口函数module_exit(xxx_exit)
所以 led.c的驱动程序中,通过编写这两个函数的内容贯穿始终。
/* 入口函数 */ |
注册/卸载字符设备
当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。
注册设备register_chrdev 函数 static inline int register_chrdev(unsigned int major,const char *name,const struct file_operations *fops)
major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
卸载设备 static inline void unregister_chrdev(unsigned int major,const char *name)
major:要注销的设备对应的主设备号。
name:要注销的设备对应的设备名。
#define LED_MAJOR 200 //设备号,不可与linux系统中的设备号重复
#define LED_NAME “led” //设备名字
|
register_chrdev函数返回小于零时,注册失败。
const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.write = led_write,
};
这里的open release write函数最终跟应用层的open write函数会对接,所以要想让应用层除了使用 加载模块函数:module_init(xxx_init); //注册模块加载函数
卸载模块函数:module_exit(xxx_exit); //注册模块卸载函数
这两个函数外的其他函数,就需要用到这个结构体中定义的函数。
这个图中的应用程序和具体驱动是需要我们写的,C库是应用层的库,他是在我们移植进去的根文件系统中,而驱动层使用的Open函数的内容虽然是我们自己写得,但他是基于file_operations结构的,这个结构是在内核中配置好的,这个结构的上层就是在建立与根文件系统之间的数据传输。具体怎么程序,看下面的 系统调用小节
地址映射
唠一唠:在硬件内存上,驱动开发所用的地址和实际引脚连接的物理地址不在同一个空间,需要进行地址映射,驱动开发使用的是虚拟地址,真正连接引脚的地址叫物理地址。
驱动程序要使用GPIO的寄存器需要先进行寄存器地址映射,物理内存和虚拟内存之间的转换,需要用到
两个函数:ioremap 和 iounmap。
物理内存只有 1GB,虚拟内存有 4GB,那么肯定存在多个虚拟地址映射到同一个物理地
址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理
/* 寄存器物理地址 */ |
声明需要使用到的寄存器物理地址,给寄存器物理地址起个名字,后面方便调用
/* 映射后的寄存器虚拟地址指针 */ |
定义了指向__iomem类型的通用的静态指针变量,void在定义指针时表示是通用的指针类型,__iomem是一个特殊的指针类型,通常用于表示硬件寄存器的地址,这些寄存器通常在内存中但需要特殊的读写权限。
这里只定义了GPIO的映射。用到GPIOI是因为LED灯是在芯片的GPIOI0的引脚上。
可以看出,GPIOI0输出低电平时,LED0点亮。
/* 地址映射 */ |
这里开始介绍函数 ioremap 函数 iounmap 函数
1、ioremap 函数
ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在
arch/arm/include/asm/io.h 文件中,定义如下:void __iomem *ioremap(resource_size_t res_cookie, size_t size);
res_cookie:要映射的物理起始地址。
size:要映射的内存空间大小。
返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址。
举例:GPIOI_MODER_PI = ioremap(GPIOI_MODER, 4); 就是将GPIOI_MODER声明的实际物理地址映射到刚才定义的静态指针变量中,4指的是4个字节,32位的寄存器。是不是PC机说得 32位系统和64位系统就是这个意思捏。查了下不是,,32位CPU一次只能处理32位数据,也就是4个字节,而64位CPU一次可以处理64位数据,也就是8个字节。
iounmap 函数
卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射,iounmap 函数原型如下:void iounmap (volatile void __iomem *addr)
iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。
配置寄存器
跟我们使用STM32逻辑开发一样,点亮LED要配置LED0引脚的时钟,输入还是输出,输出类型 推挽什么的,还有速度等等,在驱动开发中也是这样的。
注意:使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
|
readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作,参数 addr 就是要
读取写内存地址,返回值就是读取到的数据。
writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作,参数 value 是要
写入的数值,addr 是要写入的地址。
所以我们这里只用到 readl writel 32位读写的函数。
使能GPIOI时钟 MPU_AHB4_PERIPH_RCC_PI寄存器
把MPU_AHB4_PERIPH_RCC_PI寄存器的bit8位置1即可使能 GPIOI时钟
/* 2、是能GPIOI时钟*/ |
将GPIOI_0设置为输出引脚 GPIOI_MODER_PI寄存器
/* 3、将GPIOI_0设置为输出引脚 */ |
将GPIOI_0设置为推挽输出 GPIOI_OTYPER_PI寄存器
/* 4、 将GPIOI_0设置为推挽输出*/ |
这个简单,直接置0就行还
将GPIOI_0设置为超高速 GPIOI_OSPEEDR_PI寄存器
/* 5、 将GPIOI_0设置为超高速 */ |
将GPIOI_0设置为上拉 GPIOI_PUPDR_PI寄存器
00 - No pull-up, pull-down: 没有被上拉或下拉电阻配置。
01 - Pull-up: 被配置为上拉电阻。
10 - Pull-down: 被配置为下拉电阻。
11 - Reserved: 保留,不被使用。
/* 6、 将GPIOI_0设置为上拉 */ |
控制GPIOI_0输出电平1/0 GPIOx_ODR寄存器
这个配置是在控制的时候配置,初始化的时候控制用下面那个寄存器
将GPIOI_0默认输出高电平 GPIOI_BSRR_PI寄存器
BS:0-15位 只写位。读取这些位返回0x0000。当这些位为1时,它们会设置对应的ODRx位。
BR: 16-31 只写位。读取这些位返回0x0000。当这些位为1时,它们会重置对应的ODRx位。注意,如果BSx和BRx都被设置了,BSx具有优先级。
/* 7、将GPIOI_0默认输出高电平,关闭LED灯 */ |
LED入口初始化
入口程序,先是将LED用到的物理地址进行映射,然后设置寄存器地址的值,进行配置LED的参数,然后向内核注册字符设备(设备有字符设备,内存设备,网络设备,就是字面意思)
const struct file_operations led_fops = { |
编写write()函数,控制LED
控制LED还是使用前面配置寄存器时,LED初始状态的GPIOI_BSRR_PI寄存器
当然还需要获取应用层的数据,看应用层是要开灯还关灯。
前面也谈到 write()是有固定格式的,不然两边没法配合,static ssize_t led_write(struct file *filp, const char __user *buf,size_t cnt, loff_t *offt)
参数解释:
filp: 设备文件,表示打开的文件描述符 //write()函数打开的驱动文件
buf : 要写给设备写入的数据
cnt : 要写入的数据长度
offt : 相对于文件首地址的偏移
return : 写入的字节数,如果为负值,表示写入失败
字符设备读取
这里有点绕,我画个图:
因为你写程序肯定是先写驱动层,所以你会觉得有些不舒服,因为这两个函数是给应用层用得,所以在驱动层里面的write()函数是让驱动层用户去读数据,而read()函数是让驱动层用户去写进去数据。当然,这里不只使用到了write()函数,open().release() 函数也用到了,open 和 release函数主要起到保护的作用
copy_from_user()函数
读取应用层写入的数据,需要用到 unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
to:指向内核空间缓冲区的指针,这是你希望将数据复制到的位置。
from:指向用户空间数据的指针,这是你希望复制的数据。
n:要复制的字节数。
举例:
|
copy_from_user() 就把 write()应用层的 buf 数据 传递驱动层的 writebuf,cnt也是应用层发来的长度
copy_to_user()函数
返回写入的数据,当然要反馈的话,需要使用 int copy_to_user(void __user *to, const void *from, unsigned long n)
to:指向用户空间缓冲区的指针,这是你希望将数据复制到的位置。
from:指向内核空间数据的指针,这是你希望复制的数据。
n:要复制的字节数。
举例:
static char readbuf[100]; /* 读缓冲区 */ |
主要还是看应用层传过来的数据进行读取。
控制LED的程序如下:
|
应用层
应用层相对就简单多了,不需要了解底层寄存器那些就可以直接控制LED
头文件
要控制led的驱动文件,那肯定是得添加相对应的头文件,因为当时编写驱动文件的时候都是按照linux标准驱动文件编写的,如 出入函数,write函数,这些函数跟应用层也是对应的,ubuntu他本身也是linux系统,那他自然也是用得标准的驱动函数,因此,我们这边用到的函数可以通过ubuntu去查找对应的库。
man 2 xxx
xxx函数名 2是函数的类型 在2中找不到就换个数字 0 1 2 3
open()函数
先用ubuntu搜索 查下open函数的解释及库
可以看到他用到了三个头文件,至于解释很繁琐,还那么长,直接chatgpt
int open(const char *pathname, int flags);
pathname:这是要打开的文件的路径。它是一个以null结尾的字符串。
flags:这是打开文件的方式和文件访问模式的标志。这些标志可以是以下选项的组合(使用按位或运算符):
O_RDONLY:以只读方式打开文件。
O_WRONLY:以只写方式打开文件。
O_RDWR:以读写方式打开文件。
O_APPEND:在文件末尾追加数据,而不是覆盖旧数据。
O_CREAT:如果指定的文件不存在,则创建它。如果使用了这个标志,那么需要提供第三个参数,即文件权限(mode)。
O_EXCL:如果使用O_CREAT创建了一个新文件,并且文件已经存在,那么open会失败。
O_TRUNC:如果文件已经存在,那么用零填充文件并使其大小与指定的大小相同。
返回值是一个整数,代表文件描述符。如果出错,则返回-1。
write()函数
write函数只用到一个头文件
ssize_t write(int fd, const void *buf, size_t count);
fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件,open 函数打开文件成功以后会得到文件描述符。
buf:要写入的数据。
count:要写入的数据长度,也就是字节数。
返回值:写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回负值,表示写入失败。
close()函数
只需写上驱动文件名就能关闭了
应用程序
|
程序编译
驱动文件通过内核编译的环境去编译,驱动还是属于内核程序的模块,在驱动模块下创建Makefile文件
KERNELDIR := /home/laohu/linux/atk-mp1/linux/my_linux |
应用层的程序就需要通过ARM gcc编译器去编译了arm-none-linux-gnueabihf-gcc ledAPP.c -o ledAPP
系统调用(根文件系统与驱动层数据传输) 可不看
写得比较乱哈,主要是linux这一套确实是挺复杂,这才刚入门,感觉要学的东西实在太多了
上面提到,根文件系统中写的应用程序通过调用open write函数 再通过系统调用内核层的函数
下面是我简化的系统调用流程,简化了很多内容,因为我是参考的其他操作系统的,看来根文件系统也是可以调用寄存器的。
设备树
STM32MP157设备树
这个板子的设备树真的是太乱了,完全没条例,文件一环套一环,从mp157套到了mp151板子上, 我整理了大致的设备树,剩下的用到的再去查吧。
什么是设备树
设备树是用来表示linux内核连接的设备节点,因为设备节点太多了,所以它分了几个大类,如cpus,soc等,这些是大类,每个大类下又分了很多小类,如soc下的gpu iic等,每个小类下又有很多分支,分支的末端是该功能连接的最终设备,如led,蜂鸣器等,末端的叫节点。每个节点下就存放了节点的信息,如LED中会描述控制这个LED用到了哪些寄存器,来配置LED的GPIO引脚。设备树的分支从大类到小类像不像一棵树的枝干呢? 最终连接的设备就好似树上的叶子,密密麻麻。
设备树的作用
我在做了LED设备树的试验后发现,加了设备树只是比之前不加设备树多了些读取设备树的步骤,其他没什么不一样的。
从设备树节点的存放的配置信息看,他并没有什么东西,只是放了LED设备的硬件配置信息,寄存器地址这些;所以加了设备树只是提前存放这个设备的硬件配置信息,便于驱动开发的时候直接读取寄存器地址,不需要自己再去看产品手册。
设备树中的用法
#address-cells = <1>;
#size-cells = <1>;
父节点,也就是枝干节点中都会有这个,这个是描述他的子节点的地址信息的格式,如:
子结点中 reg = <0x2ffff000 0x1000>; 但看reg属性中的这两个地址,你肯定不知道是什么意思,所以你就看他的父节点#address-cells = <1>;
#size-cells = <1>; 这个意思就是说,子节点的地址信息中,第一个数据时地址,第二个是长度,
设备树下的点亮一个LED
跟之前的程序大差不差,就是在设备树的stm32mp157d-atk.dts 下的 /节点下添加个LED的设备
stm32mp1_led { |
之后在驱动开发将之前的寄存器映射改成读取设备树节点信息就ok了
/* 获取设备树中的属性数据*/ |
以前是直接映射,现在是先从设备树读取再映射。
设备树的升级使用
前面“点亮LED”使用的设备树方法,其实还是有所欠缺,实际开发也不是那样的,之前是在设备树的LED节点下储存GPIO的寄存器地址属性,其实是不需要的,设备树中的pinctrl和gpio子系统已经储存了GPIO的驱动,分了两个子系统去配置GPIO属性,pinctrl负责配置GPIO的功能选择,gpio子系统负责设置GPIO的电信号。
LED设备树配置
在 dts文件下的 /目录下添加
gpioled { |
这个超级简单方便,不需要再去找他的寄存器了,直接用查找已经配置好的GPIO属性
<&gpioi 0 GPIO_ACTIVE_LOW>; gpioi 0 指的就是 i3引脚 GPIO_ACTIVE_LOW 指该引脚默认输出低
在设备树中添加完LED的配置后,就已经实现了LED的寄存器配置,后面只需在驱动程序中读取设备树中的LED节点信息即可。
/* 获取设备树中的属性数据*/ |
分了6个步骤
**1、dtsled.nd = of_find_node_by_path(“/gpioled”); 在设备树中寻找 LED的节点信息 **
2、ret = of_property_read_string(dtsled.nd, “compatible”, &str); 获取LED节点下compatible信息if (strcmp(str, “alientek,led”)) 判断是不是跟led写得信息一样,这一步感觉在这个项目中没意义。
3、ret = of_property_read_string(dtsled.nd, “status”, &str);if (strcmp(str, “okay”)) 看节点的状态 是不是okay,okay的话就是指这个节点可以使用,也没什么意思。
4、dtsled.led_gpio = of_get_named_gpio(dtsled.nd, “led-gpio”, 0);获取reg 属性内容,我在设备树中也没写 reg的信息,应该是默认会有的吧 没意思
5、ret = gpio_request(dtsled.led_gpio, “LED-GPIO”);申请使用该设备树的GPIO
6、 ret = gpio_direction_output(dtsled.led_gpio, 1);测试 下能不能控制。
重要的步骤也就两个
应用层 程序不变
学习定时器了!
时间管理 延时、计时
mp157的默认 系统频率(节拍率)是 100 HZ 可在图形化菜单中配置 -> Kernel Features -> Timer frequency (
全局变量jiffies 系统的实时节拍数,Linux的秒表
jiffies在系统启动后,根据系统频率的快慢从0开始自增,jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统
系统节拍率是100 HZ 表示一秒jiffies增加100个节拍, jiffies/HZ 就表示系统运行时间,32 位的 jiffies 只需要 49.7 天就发生了绕回,对于 64 位的 jiffies 来说大概需要5.8 亿年才能绕回,因此 jiffies_64 的绕回忽略不计
内核定时器 软件定时器
Linux 内核定时器使用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行,和我们使用硬件定时器的套路一样,只是使用内核定时器不需要做一大堆的寄存器初始化工作。在使用内核定时器的时候要注意一点,内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
1、timer_list 结构体
Linux 内核使用 timer_list 结构体表示内核定时器,timer_list 定义在文件include/linux/timer.h 中,定义如下:
struct timer_list { |
解释:
tiemr_list 结构体的expires 成员变量表示超时时间,单位为节拍数。比如我们现在需要定义一个周期为 2 秒的定时器,那么这个定时器的超时时间就是 jiffies+(2*HZ)
function 就是定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定时处理函数,function 函数的形参就是我们定义的 timer_list 变量。
总结: 这个结构体是配置定时器最重要的地方,在这里配置好你的中断时间,以及中断函数,和中断函数的参数。所以在驱动开发中,要用到定时器先声明这个结构体以及他的头文件。
2、 API函数初始化定时器
1、timer_setup 函数 将上面配置的timer_list结构体拉进去配置
timer_setup 函数负责初始化 timer_list 类型变量,当我们定义了一个 timer_list 变量以后一定要先用 timer_setup 初始化一下。timer_setup 函数原型如下:
void timer_setup(struct timer_list *timer, void (*func)(struct timer_list *), unsigned int flags)
timer:要初始化定时器
func:定时器的回调函数,此函数的形参是当前定时器的变量。
flags: 标志位,直接给 0 就行。
2、add_timer 函数 开启定时器
买的 写了半天没保存成功 操!!!!! 没保存的就不写了 我真服了他妈的