开发环境

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!”

驱动层

upload successful

驱动模块的加载与卸载

  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的驱动程序中,通过编写这两个函数的内容贯穿始终。

/* 入口函数 */
static int __init chrdevbase_init(void)
{
int ret = 0;

return ret;
}


/* 出口函数 */
static void __exit chrdevbase_exit(void)
{
...
}

/* 驱动的注册与卸载 */
module_init(chrdevbase_init); /* 入口函数 */
module_exit(chrdevbase_exit); /* 出口函数 */

注册/卸载字符设备

当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。

注册设备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” //设备名字


/* 向内核注册字符设备 */
ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if (ret < 0)
{
printk("chrdevbase driver register failed!\r\n");
goto faile_register;
}

faile_register:
return -EIO;

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); //注册模块卸载函数这两个函数外的其他函数,就需要用到这个结构体中定义的函数。

upload successful

这个图中的应用程序和具体驱动是需要我们写的,C库是应用层的库,他是在我们移植进去的根文件系统中,而驱动层使用的Open函数的内容虽然是我们自己写得,但他是基于file_operations结构的,这个结构是在内核中配置好的,这个结构的上层就是在建立与根文件系统之间的数据传输。具体怎么程序,看下面的 系统调用小节

地址映射

唠一唠:在硬件内存上,驱动开发所用的地址和实际引脚连接的物理地址不在同一个空间,需要进行地址映射,驱动开发使用的是虚拟地址,真正连接引脚的地址叫物理地址。
驱动程序要使用GPIO的寄存器需要先进行寄存器地址映射,物理内存和虚拟内存之间的转换,需要用到
两个函数:ioremap 和 iounmap。

upload successful

物理内存只有 1GB,虚拟内存有 4GB,那么肯定存在多个虚拟地址映射到同一个物理地
址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理

/* 寄存器物理地址 */
#define PERIPH_BASE (0x40000000) //起始地址
#define MPU_AHB4_PERIPH_BASE (PERIPH_BASE + 0x10000000) //起始地址
#define RCC_BASE (MPU_AHB4_PERIPH_BASE + 0x0000) //起始地址
#define RCC_MP_AHB4ENSETR (RCC_BASE + 0XA28) //gpio使能

#define GPIOI_BASE (MPU_AHB4_PERIPH_BASE + 0xA000) //起始地址
#define GPIOI_MODER (GPIOI_BASE + 0x0000) //设置输入输出
#define GPIOI_OTYPER (GPIOI_BASE + 0x0004) //输出类型 推挽/开路
#define GPIOI_OSPEEDR (GPIOI_BASE + 0x0008) //时钟速度
#define GPIOI_PUPDR (GPIOI_BASE + 0x000C) //上拉还是下拉
#define GPIOI_BSRR (GPIOI_BASE + 0x0018) //控制IO引脚高低电平

声明需要使用到的寄存器物理地址,给寄存器物理地址起个名字,后面方便调用

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *MPU_AHB4_PERIPH_RCC_PI;
static void __iomem *GPIOI_MODER_PI;
static void __iomem *GPIOI_OTYPER_PI;
static void __iomem *GPIOI_OSPEEDR_PI;
static void __iomem *GPIOI_PUPDR_PI;
static void __iomem *GPIOI_BSRR_PI;

定义了指向__iomem类型的通用的静态指针变量,void在定义指针时表示是通用的指针类型,__iomem是一个特殊的指针类型,通常用于表示硬件寄存器的地址,这些寄存器通常在内存中但需要特殊的读写权限。
这里只定义了GPIO的映射。用到GPIOI是因为LED灯是在芯片的GPIOI0的引脚上。

upload successful
upload successful

可以看出,GPIOI0输出低电平时,LED0点亮。

/* 地址映射 */
static void led_ioremap(void)
{
MPU_AHB4_PERIPH_RCC_PI = ioremap(RCC_MP_AHB4ENSETR, 4);
GPIOI_MODER_PI = ioremap(GPIOI_MODER, 4);
GPIOI_OTYPER_PI = ioremap(GPIOI_OTYPER, 4);
GPIOI_OSPEEDR_PI = ioremap(GPIOI_OSPEEDR, 4);
GPIOI_PUPDR_PI = ioremap(GPIOI_PUPDR, 4);
GPIOI_BSRR_PI = ioremap(GPIOI_BSRR, 4);
}

/* 取消映射 */
static void led_iounmap(void)
{
iounmap(MPU_AHB4_PERIPH_RCC_PI);
iounmap(GPIOI_MODER_PI);
iounmap(GPIOI_OTYPER_PI);
iounmap(GPIOI_OSPEEDR_PI);
iounmap(GPIOI_PUPDR_PI);
iounmap(GPIOI_BSRR_PI);
}

这里开始介绍函数 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 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。


读取:
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

写入:
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)

readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作,参数 addr 就是要
读取写内存地址,返回值就是读取到的数据。
writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作,参数 value 是要
写入的数值,addr 是要写入的地址。
所以我们这里只用到 readl writel 32位读写的函数。

使能GPIOI时钟 MPU_AHB4_PERIPH_RCC_PI寄存器

upload successful

把MPU_AHB4_PERIPH_RCC_PI寄存器的bit8位置1即可使能 GPIOI时钟

/* 2、是能GPIOI时钟*/
val = readl(MPU_AHB4_PERIPH_RCC_PI);
val &= ~(0x1 << 8); /* 清除bit8以前的设置 */ 这一步感觉没必要啊!需要清零吗?
val |= (0x1 << 8); /* 将bit8设置为1 */
writel(val, MPU_AHB4_PERIPH_RCC_PI);

将GPIOI_0设置为输出引脚 GPIOI_MODER_PI寄存器

upload successful

/* 3、将GPIOI_0设置为输出引脚 */ 
val = readl(GPIOI_MODER_PI);
val &= ~(0x3 << 0); /* 将bit1和bit0清零 */ 这儿的 0x3改成0x2更逻辑些
val |= (0x1 << 0); /* 将bit1和bit0设置为01 */
writel(val, GPIOI_MODER_PI);

将GPIOI_0设置为推挽输出 GPIOI_OTYPER_PI寄存器

upload successful

/* 4、 将GPIOI_0设置为推挽输出*/
val = readl(GPIOI_OTYPER_PI);
val &= ~(0x1 << 0); /* bit0清零 */
writel(val, GPIOI_OTYPER_PI);

这个简单,直接置0就行还

将GPIOI_0设置为超高速 GPIOI_OSPEEDR_PI寄存器

upload successful

/* 5、 将GPIOI_0设置为超高速 */
val = readl(GPIOI_OSPEEDR_PI);
val &= ~(0x3 << 0); /* 将bit1和bit0清零 */
val |= (0x3 << 0); /* 将bit1和bit0设置为11 */
writel(val, GPIOI_OSPEEDR_PI);

将GPIOI_0设置为上拉 GPIOI_PUPDR_PI寄存器

upload successful

00 - No pull-up, pull-down: 没有被上拉或下拉电阻配置。
01 - Pull-up: 被配置为上拉电阻。
10 - Pull-down: 被配置为下拉电阻。
11 - Reserved: 保留,不被使用。

/* 6、 将GPIOI_0设置为上拉 */
val = readl(GPIOI_PUPDR_PI);
val &= ~(0x3 << 0); /* 将bit1和bit0清零 */
val |= (0x1 << 0); /* 将bit1和bit0设置为10 */
writel(val, GPIOI_PUPDR_PI);

控制GPIOI_0输出电平1/0 GPIOx_ODR寄存器

upload successful

这个配置是在控制的时候配置,初始化的时候控制用下面那个寄存器

将GPIOI_0默认输出高电平 GPIOI_BSRR_PI寄存器

upload successful

BS:0-15位 只写位。读取这些位返回0x0000。当这些位为1时,它们会设置对应的ODRx位。
BR: 16-31 只写位。读取这些位返回0x0000。当这些位为1时,它们会重置对应的ODRx位。注意,如果BSx和BRx都被设置了,BSx具有优先级。

/* 7、将GPIOI_0默认输出高电平,关闭LED灯 */
val = readl(GPIOI_BSRR_PI);
val &= ~(0x1 << 0); /* 将bit0清零 */
val |= (0x1 << 0); /* 将bit0设置为1 */
writel(val, GPIOI_BSRR_PI);

LED入口初始化

入口程序,先是将LED用到的物理地址进行映射,然后设置寄存器地址的值,进行配置LED的参数,然后向内核注册字符设备(设备有字符设备,内存设备,网络设备,就是字面意思)

const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.write = led_write,
};

/* 入口函数 */
static int __init led_init(void)
{
int ret = 0;
u32 val = 0;

/* 1、寄存器地址映射 */
led_ioremap();

/* 寄存器配置 就是上面的代码 不复述了 */



/* 注册字符设备 */
printk("led_init\r\n");

/* 向内核注册字符设备 */
ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if (ret < 0) {
printk("chrdevbase driver register failed!\r\n");
goto faile_register;
}

return 0;

faile_register:
return -EIO;
}

编写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 : 写入的字节数,如果为负值,表示写入失败

字符设备读取

这里有点绕,我画个图:

upload successful

因为你写程序肯定是先写驱动层,所以你会觉得有些不舒服,因为这两个函数是给应用层用得,所以在驱动层里面的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:要复制的字节数。

举例:


static char writebuf[100]; /* 写缓冲区 */

static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
int ret = 0;

//printk("chrdevbase_write\r\n");

ret = copy_from_user(writebuf, buf, cnt);
if(ret == 0) {
printk("kernel recevedata:%s\r\n", writebuf);
} else {
printk("kernel recevedata failed!\r\n");
}

return 0;
}

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];  /* 读缓冲区 */
static char kerneldata[] = {"kernel data!"}; /* 应用从内核读取到的数据 */

static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt,
loff_t *offt)
{
int ret = 0;

memcpy(readbuf, kerneldata, sizeof(kerneldata));

ret = copy_to_user(buf, readbuf, cnt);
if(ret == 0) {
printk("kernel senddata ok!\r\n");
} else {
printk("kernel senddata failed!\r\n");
}

//printk("chrdevbase_read\r\n");
return 0;

}

主要还是看应用层传过来的数据进行读取。

控制LED的程序如下:


/* 打开或关闭LED */
void led_switch(u8 sta)
{
u32 val = 0;

if(sta == LEDON) {
val = readl(GPIOI_BSRR_PI);
val &= ~(0x1 << 16); /* 将bit16零 */
val |= (0x1 << 16); /* 将bit16设置为1 */
writel(val, GPIOI_BSRR_PI);
} else if(sta == LEDOFF) {
val = readl(GPIOI_BSRR_PI);
val &= ~(0x1 << 0); /* 将bit0清零 */
val |= (0x1 << 0); /* 将bit0设置为1 */
writel(val, GPIOI_BSRR_PI);
}
}

static ssize_t led_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
int ret = 0;
unsigned char databuf[1];
unsigned char ledstat;

ret = copy_from_user(databuf, buf, cnt);
if(ret < 0) {
printk("kernel write failed!\r\n");
ret = -EFAULT;
}

ledstat = databuf[0]; /* 获取到应用传递进来的开关灯状态 */

if(ledstat == LEDON) { /* 开灯 */
led_switch(LEDON);
} else if(ledstat == LEDOFF) { /* 关灯 */
led_switch(LEDOFF);
}

return ret;
}

应用层

应用层相对就简单多了,不需要了解底层寄存器那些就可以直接控制LED

upload successful

头文件

  要控制led的驱动文件,那肯定是得添加相对应的头文件,因为当时编写驱动文件的时候都是按照linux标准驱动文件编写的,如 出入函数,write函数,这些函数跟应用层也是对应的,ubuntu他本身也是linux系统,那他自然也是用得标准的驱动函数,因此,我们这边用到的函数可以通过ubuntu去查找对应的库。
man 2 xxx xxx函数名 2是函数的类型 在2中找不到就换个数字 0 1 2 3

open()函数

先用ubuntu搜索 查下open函数的解释及库

upload successful

可以看到他用到了三个头文件,至于解释很繁琐,还那么长,直接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()函数

upload successful

write函数只用到一个头文件

ssize_t write(int fd, const void *buf, size_t count);

fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件,open 函数打开文件成功以后会得到文件描述符。
buf:要写入的数据。
count:要写入的数据长度,也就是字节数。
返回值:写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回负值,表示写入失败。

close()函数

upload successful

只需写上驱动文件名就能关闭了

应用程序


#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#define LEDOFF 0
#define LEDON 1

int main(int argc, char *argv[])
{
int fd, retvalue;
unsigned char databuf[1];
char *filename;

if(argc !=3 ) {
printf("Error Usage!\r\n");
return -1;
}

filename = argv[1];

/* 打开文件 */
fd = open(filename, O_RDWR); /* 以读写的方式打开文件 */
if (fd < 0) {
printf("Can't open file %s\r\n", filename);
return -1;
}

databuf[0] = atoi(argv[2]); /* 写入的数据,是数字的,表示打开或关闭 */
retvalue = write(fd, databuf, 1);

if(retvalue < 0) {
printf("write file %s \r\n", filename);
close(fd);
return -1;
}

/* 关闭文件 */
retvalue = close(fd);
if(retvalue < 0) {
printf("Can't close file %s\r\n", filename);
return -1;
}

return 0;
}

程序编译

驱动文件通过内核编译的环境去编译,驱动还是属于内核程序的模块,在驱动模块下创建Makefile文件

KERNELDIR := /home/laohu/linux/atk-mp1/linux/my_linux
CURRENT_PATH := $(shell pwd)
obj-m := led.o

build: kernel_modules

kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

应用层的程序就需要通过ARM gcc编译器去编译了
arm-none-linux-gnueabihf-gcc ledAPP.c -o ledAPP

系统调用(根文件系统与驱动层数据传输) 可不看

写得比较乱哈,主要是linux这一套确实是挺复杂,这才刚入门,感觉要学的东西实在太多了
上面提到,根文件系统中写的应用程序通过调用open write函数 再通过系统调用内核层的函数

下面是我简化的系统调用流程,简化了很多内容,因为我是参考的其他操作系统的,看来根文件系统也是可以调用寄存器的。

upload successful

设备树

STM32MP157设备树

这个板子的设备树真的是太乱了,完全没条例,文件一环套一环,从mp157套到了mp151板子上, 我整理了大致的设备树,剩下的用到的再去查吧。

upload successful

什么是设备树

  设备树是用来表示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 {
compatible = "atkstm32mp1-led";
status = "okay";
reg = <0X50000A28 0X04 /* RCC_MP_AHB4ENSETR 使能GPIOI时钟*/
0X5000A000 0X04 /* GPIOI_MODER GPIOI_0设置为输出引脚*/
0X5000A004 0X04 /* GPIOI_OTYPER GPIOI_0设置为推挽输出*/
0X5000A008 0X04 /* GPIOI_OSPEEDR GPIOI_0设置为超高速*/
0X5000A00C 0X04 /* GPIOI_PUPDR GPIOI_0设置为上拉*/
0X5000A018 0X04 >; /* GPIOI_BSRR GPIOI_0 输出*/
};

之后在驱动开发将之前的寄存器映射改成读取设备树节点信息就ok了

/* 获取设备树中的属性数据*/

/* 1、获取设备节点:stm32mp1_led */
dtsled.nd = of_find_node_by_path("/stm32mp1_led");
if(dtsled.nd == NULL) {
printk("stm32mp1_led node nost find!\r\n");
return -EINVAL;
} else {
printk("stm32mp1_lcd node find!\r\n");
}

/* 2、获取 compatible 属性内容 */
proper = of_find_property(dtsled.nd, "compatible", NULL);
if(proper == NULL) {
printk("compatible property find failed\r\n");
} else {
printk("compatible = %s\r\n", (char*)proper->value);
}

/* 3、获取 status 属性内容 */
ret = of_property_read_string(dtsled.nd, "status", &str);
if(ret < 0){
printk("status read failed!\r\n");
} else {
printk("status = %s\r\n",str);
}

/* 4、获取 reg 属性内容 */
ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 12);
if(ret < 0) {
printk("reg property read failed!\r\n");
} else {
u8 i = 0;
printk("reg data:\r\n");
for(i = 0; i < 12; i++)
printk("%#X ", regdata[i]);
printk("\r\n");
}

/* 1、寄存器地址映射 */
MPU_AHB4_PERIPH_RCC_PI = of_iomap(dtsled.nd, 0);
GPIOI_MODER_PI = of_iomap(dtsled.nd, 1);
GPIOI_OTYPER_PI = of_iomap(dtsled.nd, 2);
GPIOI_OSPEEDR_PI = of_iomap(dtsled.nd, 3);
GPIOI_PUPDR_PI = of_iomap(dtsled.nd, 4);
GPIOI_BSRR_PI = of_iomap(dtsled.nd, 5);

以前是直接映射,现在是先从设备树读取再映射。

设备树的升级使用

  前面“点亮LED”使用的设备树方法,其实还是有所欠缺,实际开发也不是那样的,之前是在设备树的LED节点下储存GPIO的寄存器地址属性,其实是不需要的,设备树中的pinctrl和gpio子系统已经储存了GPIO的驱动,分了两个子系统去配置GPIO属性,pinctrl负责配置GPIO的功能选择,gpio子系统负责设置GPIO的电信号。

LED设备树配置

在 dts文件下的 /目录下添加

gpioled {
compatible = "alientek,led";
status = "okay";
led-gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;
};

这个超级简单方便,不需要再去找他的寄存器了,直接用查找已经配置好的GPIO属性
<&gpioi 0 GPIO_ACTIVE_LOW>; gpioi 0 指的就是 i3引脚 GPIO_ACTIVE_LOW 指该引脚默认输出低

在设备树中添加完LED的配置后,就已经实现了LED的寄存器配置,后面只需在驱动程序中读取设备树中的LED节点信息即可。

/* 获取设备树中的属性数据*/

/* 1、获取设备节点:stm32mp1_led */
dtsled.nd = of_find_node_by_path("/gpioled");
if(dtsled.nd == NULL) {
printk("gpioled node nost find!\r\n");
return -EINVAL;
} else {
printk("gpioled node find!\r\n");
}

/* 2、获取 compatible 属性内容 */
ret = of_property_read_string(dtsled.nd, "compatible", &str);
if(ret < 0) {
printk("gpioled: Failed to get compatible property\n");
return -EINVAL;
}
if (strcmp(str, "alientek,led")) {
printk("gpioled: Compatible match failed\n");
return -EINVAL;
}

/* 3、获取 status 属性内容 */
ret = of_property_read_string(dtsled.nd, "status", &str);
if(ret < 0){
printk("status read failed!\r\n");
}
if (strcmp(str, "okay"))
return -EINVAL;

/* 4、获取 reg 属性内容 */
dtsled.led_gpio = of_get_named_gpio(dtsled.nd, "led-gpio", 0);
if(dtsled.led_gpio < 0) {
printk("can't get led-gpio");
return -EINVAL;
}
printk("led-gpio num = %d\r\n", dtsled.led_gpio);

/* 5.向 gpio 子系统申请使用 GPIO */
ret = gpio_request(dtsled.led_gpio, "LED-GPIO");
if (ret) {
printk(KERN_ERR "gpioled: Failed to request led-gpio\n");
return ret;
}

/* 6、设置 PI0 为输出,并且输出高电平,默认关闭 LED 灯 */
ret = gpio_direction_output(dtsled.led_gpio, 1);
if(ret < 0) {
printk("can't set gpio!\r\n");
}

分了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 ( [=y])

全局变量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 {

struct hlist_node entry;
unsigned long expires; /* 定时器超时时间,单位是节拍数*/
void (*function)(struct timer_list *); /* 定时处理函数*/
u32 flags; /* 标志位*/


#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};

解释:
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 函数 开启定时器

买的 写了半天没保存成功 操!!!!! 没保存的就不写了 我真服了他妈的