Linux第83步_采用“Linux内核定时器”点灯以及相关API函数

“Linux内核定时器”是采用“系统时钟”来实现的。它不是周期性运行的,一旦发生超时就会自动关闭。如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。

Limux内核使用全局变量jiffies来记录“系统从启动以来的系统节拍数”,系统启动的时候会将jiffies初始化为0。

需要包含#include <linux/jiffies.h>头文件。

extern u64 __cacheline_aligned_in_smp jiffies_64;

extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies

jiffies为jiffies_64的低32位,用来记录系统从启动以来的“系统节拍数”。见下图:

1、设置系统节拍率

1)、输入“cd /home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31/回车”,切换到“/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31/”目录下

2)、输入“ls回车”,

查看“/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31/”目录下的所有文件和文件夹

记住:“stm32mp1_atk_defconfig”位于“/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31/arch/arm/configs”

记住:“make menuconfig”用于打开linux内核图形化配置界面

输入“make menuconfig回车”,打开linux内核图形化配置界面

移动向下光标键至“Kernel Features”,见下图:

按“回车键”,得到下图:

移动向下光标键至“Timer frequency”,见下图:

按“回车键”,得到下图:

移动向下光标键至合适的“系统节拍率”。然后按下“回车键”就可以选中了。默认选择100Hz,不要选择“高节拍率”,因为那样做会引起频繁中断,加剧系统负担。因此,不需要修改,按“ESC键”,直至关闭“linux内核图形化配置界面”。

2、处理Linux内核定时器的回绕函数

time_after(unkown, known)

如果unkown>known,超过预设的系统节拍数,time_after()返回真

time_before(unkown, known)

如果unkown<known,没有超过预设的系统节拍数,time_before()返回真

time_after_eq(unkown, known)

如果unkown>=known,超过预设的系统节拍数,time_after_eq()返回真

time_before_eq(unkown, known)

如果unkown<=known,没有超过预设的系统节拍数,time_before_eq()返回真

3、Linux内核定时器的“计数器jiffies”和ms、us、ns之间的转换函数

unsigned int jiffies_to_msecs(const unsigned long j);

//将jiffies类型的参数 j转换为对应的毫秒;

unsigned int jiffies_to_usecs(const unsigned long j);

//将jiffies类型的参数 j转换为对应的微秒;

u64 jiffies_to_nsecs(const unsigned long j);

//将jiffies类型的参数 j转换为对应的纳秒;

unsigned long msecs_to_jiffies(const unsigned int m);

//将毫秒转换为jiffies类型的数据;

unsigned long usecs_to_jiffies(const unsigned int u);

//将微秒转换为jiffies类型的数据;

unsigned long nsecs_to_jiffies(u64 n);

//将纳秒转换为jiffies类型的数据;

4、Linux内核定时器timer_list结构体

Linux内核使用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

};

5、Linux内核定时器初始化函数

void timer_setup(struct timer_list *timer, void (*func)(struct timer_list *), unsigned int flags)

timer指向要初始化的定时器

func为定时器的回调函数

flags为标志位

负责初始化“timer_list结构变量”

void add_timer(struct timer_list *timer)

timer指向要注册的定时器

向Linux内核注册定时器

int del_timer(struct timer_list * timer)

timer指向要被删除的定时器

用于删除一个定时器,返回0表示定时器没有被激活;返回1表示定时器被激活。

int del_timer_sync(struct timer_list *timer)

timer指向要被删除的定时器

等待其他处理器使用完定时器后再删除该定时器,del_timer_sync()不能用在中断服务程序中;

int mod_timer(struct timer_list *timer, unsigned long expires)

timer:要修改超时时间(定时值)的定时器;

expires:修改后的超时时间;

用于修改定时值,如果定时器还没有激活的话,mod_timer()函数会激活定时器;

返回值为0表示调用mod_timer()函数前,该定时器未被激活;

返回值为1表示调用mod_timer()函数前,该定时器已被激活;

举例:

struct timer_list timer; /* 定义定时器 */

/* 定时器回调函数 */

void timer_function(struct timer_list *arg)

{

  /*

  * 定时器处理代码

  */

  /* 如果需要定时器周期性运行的话就使用mod_timer()

  * 函数重新设置超时值并且启动定时器。

  */

  mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000));

}

/* 初始化函数 */

void init(void)

{

  timer_setup(&timerdev.timer, timer_function, 0); /* 初始化定时器 */

  timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间2秒 */

  add_timer(&timer); /*向Linux内核注册定时器,启动定时器 */

}

/* 退出函数 */

void exit(void)

{

  del_timer(&timer); /* 删除定时器 */

  /* 或者使用del_timer_sync(&timer) */

}

6、Linux内核短延时函数,需要包含头文件“#include <delay.h>

void ndelay(unsigned long nsecs)延时nsecs个纳秒;

void udelay(unsigned long usecs) 延时usecs个微秒;

void mdelay(unsigned long mseces) 延时mseces个毫秒;

1)、添加“gpio_led”节点

打开虚拟机上“VSCode”,点击“文件”,点击“打开文件夹”,点击“zgq”,点击“linux”,点击“atk-mp1”,点击“linux”,点击“my_linux”,点击“linux-5.4.31”,点击“确定”,点击“stm32mp157d-atk.dts”。

stm32mp157d-atk.dts文件如下:

/dts-v1/;

#include "stm32mp157.dtsi"

#include "stm32mp15xd.dtsi"

#include "stm32mp15-pinctrl.dtsi"

#include "stm32mp15xxaa-pinctrl.dtsi"

#include "stm32mp157-m4-srm.dtsi"

#include "stm32mp157-m4-srm-pinctrl.dtsi"

#include "stm32mp157d-atk.dtsi"

/ {

model = "STMicroelectronics STM32MP157D eval daughter";

/*model属性用于描述开发板的名字或设备模块的信息*/

compatible = "st,stm32mp157d-ed1", "st,stm32mp157";

/*compatible属性用于将设备和驱动绑定起来*/

chosen {  /*chosen子节点*/

stdout-path = "serial0:115200n8";

};

aliases {    /*aliases子节点*/

serial0 = &uart4;

    /*给&uart4起个别名叫“serial0”*/

};

reserved-memory {

gpu_reserved: gpu@f6000000 {  /*gpu节点标签为gpu_reserved*/

reg = <0xf6000000 0x8000000>;

no-map;

};

optee_memory: optee@fe000000 {

reg = <0xfe000000 0x02000000>;

no-map;

};

};

stm32mp1_led {

compatible = "atkstm32mp1-led";

/*compatible属性用于将设备stm32mp1_led和驱动“.ko”绑定起来*/

status = "okay";

reg = <0X50000A28 0X04 /* RCC_MP_AHB4ENSETR */

0X5000A000 0X04 /* GPIOI_MODER */

0X5000A004 0X04 /* GPIOI_OTYPER */

0X5000A008 0X04 /* GPIOI_OSPEEDR */

0X5000A00C 0X04 /* GPIOI_PUPDR */

0X5000A018 0X04 >; /* GPIOI_BSRR */

};

};

&cpu1{

cpu-supply = <&vddcore>;

};

&gpu {

contiguous-area = <&gpu_reserved>;

status = "okay";

};

&optee {

status = "okay";

};

修改后的stm32mp157d-atk.dts文件如下:

/dts-v1/;

#include "stm32mp157.dtsi"

#include "stm32mp15xd.dtsi"

#include "stm32mp15-pinctrl.dtsi"

#include "stm32mp15xxaa-pinctrl.dtsi"

#include "stm32mp157-m4-srm.dtsi"

#include "stm32mp157-m4-srm-pinctrl.dtsi"

#include "stm32mp157d-atk.dtsi"

/ {

model = "STMicroelectronics STM32MP157D eval daughter";

/*model属性用于描述开发板的名字或设备模块的信息*/

compatible = "st,stm32mp157d-ed1", "st,stm32mp157";

/*compatible属性用于将设备和驱动绑定起来*/

chosen {  /*chosen子节点*/

stdout-path = "serial0:115200n8";

};

aliases {    /*aliases子节点*/

serial0 = &uart4;

    /*给&uart4起个别名叫“serial0”*/

};

reserved-memory {

gpu_reserved: gpu@f6000000 {  /*gpu节点标签为gpu_reserved*/

reg = <0xf6000000 0x8000000>;

no-map;

};

optee_memory: optee@fe000000 {

reg = <0xfe000000 0x02000000>;

no-map;

};

};

stm32mp1_led {

compatible = "atkstm32mp1-led";

/*compatible属性用于将设备stm32mp1_led和驱动“.ko”绑定起来*/

status = "okay";

reg = <0X50000A28 0X04 /* RCC_MP_AHB4ENSETR */

0X5000A000 0X04 /* GPIOI_MODER */

0X5000A004 0X04 /* GPIOI_OTYPER */

0X5000A008 0X04 /* GPIOI_OSPEEDR */

0X5000A00C 0X04 /* GPIOI_PUPDR */

0X5000A018 0X04 >; /* GPIOI_BSRR */

};

gpio_led {

compatible = "zgq,led";

status = "okay";

led-gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;

};

};

&cpu1{

cpu-supply = <&vddcore>;

};

&gpu {

contiguous-area = <&gpu_reserved>;

status = "okay";

};

&optee {

status = "okay";

};

2)、编译设备树

、在VSCode终端,输入“make dtbs回车”,执行编译设备树

②、输入“ls arch/arm/boot/uImage -l

查看是否生成了新的“uImage”文件

③、输入“ls arch/arm/boot/dts/stm32mp157d-atk.dtb -l

查看是否生成了新的“stm32mp157d-atk.dtb”文件

拷贝输出的文件:

④、输入“cp arch/arm/boot/uImage /home/zgq/linux/atk-mp1/linux/bootfs/ -f回车”,执行文件拷贝,准备烧录到EMMC;

⑤、输入“cp arch/arm/boot/dts/stm32mp157d-atk.dtb /home/zgq/linux/atk-mp1/linux/bootfs/ -f回车”,执行文件拷贝,准备烧录到EMMC

⑥、输入“cp arch/arm/boot/uImage /home/zgq/linux/tftpboot/ -f回车”,执行文件拷贝,准备从tftp下载;

⑦、输入“cp arch/arm/boot/dts/stm32mp157d-atk.dtb /home/zgq/linux/tftpboot/ -f回车”,执行文件拷贝,准备从tftp下载;

⑧、输入“ls -l /home/zgq/linux/atk-mp1/linux/bootfs/回车”,查看“/home/zgq/linux/atk-mp1/linux/bootfs/”目录下的所有文件和文件夹

⑨、输入“ls -l /home/zgq/linux/tftpboot/回车”,查看“/home/zgq/linux/tftpboot/”目录下的所有文件和文件夹

输入“chmod 777 /home/zgq/linux/tftpboot/stm32mp157d-atk.dtb回车

给“stm32mp157d-atk.dtb”文件赋予可执行权限

输入“chmod 777 /home/zgq/linux/tftpboot/uImage回车 ,给“uImage”文件赋予可执行权限

输入“ls /home/zgq/linux/tftpboot/回车”,查看“/home/zgq/linux/tftpboot/”目录下的所有文件和文件夹

3)、查看“gpio_led节点

启动开发板,从网络下载程序

输入“root

输入“cd /proc/device-tree/回车

切换到“/sys/firmware/devicetree/base”目录

输入“ls”查看“gpio_led”是否存在

输入“cd gpio_led/回车

输入“ls”查看“/sys/firmware/devicetree/base/gpio_led”目录下的文件和文件夹

输入“cat compatible回车

输入“cat led-gpio回车

输入“cat name回车

输入“cat status回车

4)、创建MyTimer目录

输入“cd /home/zgq/linux/Linux_Drivers/回车

切换到“/home/zgq/linux/Linux_Drivers/”目录

输入“mkdir MyTimer回车”,创建“MyTimer”目录

输入“ls回车”查看“/home/zgq/linux/Linux_Drivers/”目录下的文件和文件夹

5)、添加“LED.c”

#include "LED.h"
#include <linux/gpio.h>
//使能gpio_request(),gpio_free(),gpio_direction_input(),
//使能gpio_direction_output(),gpio_get_value(),gpio_set_value()
#include <linux/of_gpio.h>
//使能of_gpio_named_count(),of_gpio_count(),of_get_named_gpio()

struct MyLED_dev  strMyLED;

int Get_gpio_led_num(void);
int led_GPIO_request(void);
void MyLED_free(void);
void led_OnOff(u8 sta);

int Get_gpio_led_num(void)
{
  int ret = 0;
  const char *str;

  /* 设置LED所使用的GPIO */
  /* 1、获取设备节点:strMyLED */
  strMyLED.nd = of_find_node_by_path("/gpio_led");
  //path="/gpio_led,使用“全路径的节点名“在“stm32mp157d-atk.dts“中查找节点“gpio_led”
  //返回值:返回找到的节点,如果为NULL,表示查找失败。
  if(strMyLED.nd == NULL) {
    printk("gpio_led node not find!\r\n");
    return -EINVAL;
  }

  /* 2.读取status属性 */
  ret = of_property_read_string(strMyLED.nd, "status", &str);
  //在gpio_led节点中,status = "okay";
  //指定的设备节点strMyLED.nd
  //proname="status",给定要读取的属性名字
  //out_string=str:返回读取到的属性值
  //返回值:0,读取成功,负值,读取失败。
  if(ret < 0) return -EINVAL;
  if (strcmp(str, "okay")) return -EINVAL;
  //strcmp(s1,s2),当s1<s2时,返回值为负数
  //strcmp(s1,s2),当s1>2时,返回值为正数
  //strcmp(s1,s2),当s1=s2时,返回值为0

  /* 3、获取compatible属性值并进行匹配 */
  ret = of_property_read_string(strMyLED.nd, "compatible", &str);
  //在gpio_led节点中,compatible = "zgq,led";
  //指定的设备节点strMyLED.nd
  //proname="compatible",给定要读取的属性名字
  //out_string=str:返回读取到的属性值
  //返回值:0,读取成功,负值,读取失败。
  if(ret < 0) {
    printk("gpio_led node: Failed to get compatible property\n"); 
    return -EINVAL;
  }
  if (strcmp(str, "zgq,led")) {
    printk("gpio_led node: Compatible match failed\n");
    return -EINVAL;
  }

  /* 4、 根据设备树中的"led-gpio"属性,得到LED所使用的LED编号 */
  strMyLED.led_gpio_number = of_get_named_gpio(strMyLED.nd, "led-gpio", 0);
  //在gpio_led节点中,led-gpio = <&gpioi 0 GPIO_ACTIVE_LOW>
  //np=strMyLED.nd,指定的“设备节点”
  //propname="led-gpio",给定要读取的属性名字
  //Index=0,给定的GPIO索引为0
  //返回值:正值,获取到的GPIO编号;负值,失败。
  if(strMyLED.led_gpio_number < 0) {
    printk("can't get led-gpio");
    return -EINVAL;
  }
  printk("led-gpio num = %d\r\n", strMyLED.led_gpio_number);
  //打印结果为:“led-gpio num = 128“
  //因为GPIO编号是从0开始的,GPIOI端口的序号是8,每个端口有16个IO口,因此GPIOI0的编号为8*16=128

  return 0;
}

int led_GPIO_request(void)
{
  int ret = 0;

  /* 5.向gpio子系统申请使用“gpio编号” */
  ret = gpio_request(strMyLED.led_gpio_number, "LED-GPIO");
  //gpio=strMyLED.led_gpio_number,指定要申请的“gpio编号”
  //Iabel="LED-GPIO",给这个gpio引脚设置个名字为"LED-GPIO"
  //返回值:0,申请“gpio编号”成功;其他值,申请“gpio编号”失败;
  if (ret) {
    printk(KERN_ERR "strMyLED: Failed to request led-gpio\n");
    return ret;
  }

  /* 6、设置PI0为输出,并且输出高电平,默认关闭LED灯 */
  ret = gpio_direction_output(strMyLED.led_gpio_number, 1);
  //gpio=strMyLED.led_gpio_number,指定的“gpio编号”,这里是128,对应的是GI0引脚
  //value=1,设置引脚输出高电平
  //返回值:0,设置“引脚输出为vakued的值”成功;负值,设置“引脚输出为vakued的值”失败。
  if(ret < 0) {
    printk("can't set gpio!\r\n");
  }

  return 0;
}

//函数功能:释放MyLED的gpio
void MyLED_free(void)
{
  gpio_free(strMyLED.led_gpio_number);
}

void led_OnOff(u8 sta)
{
  gpio_set_value(strMyLED.led_gpio_number, sta);
  //sta=0,写入0,打开LED灯
  //sta=1,写入1,关闭LED灯	
}

6)、添加“LED.h”

#ifndef __LED_H
#define __LED_H

#include <linux/types.h>
/*
数据类型重命名
使能bool,u8,u16,u32,u64, uint8_t, uint16_t, uint32_t, uint64_t
使能s8,s16,s32,s64,int8_t,int16_t,int32_t,int64_t
*/
#include <linux/of.h>   //使能device_node结构

struct MyLED_dev{
  struct device_node *nd; /*设备节点*/
  int led_gpio_number;   /*led所使用的GPIO编号*/
};
extern struct MyLED_dev strMyLED;

extern int Get_gpio_led_num(void);
extern int led_GPIO_request(void);
extern void MyLED_free(void);
extern void led_OnOff(u8 sta);

#endif

7)、添加“LinuxTimer.c”

#include "LinuxTimer.h"
#include "LED.h"
#include <linux/timer.h>
//使能timer_list结构
//使能timer_setup(),del_timer(),del_timer_sync()
//使能add_timer(),mod_timer()

struct Timer_dev strTimer;

void Timeperiod_Init(void);
void Timer_Config(unsigned int cmd, unsigned long period);
void Delete_Timer(void);
void Timer_function(struct timer_list *arg);
void Timer_lock_Init(void);
void Set_Timer(void);

void Timeperiod_Init(void)
{
  strTimer.timeperiod=1000; /* 默认周期为1s */
}

void Timer_Config(unsigned int cmd, unsigned long period)
{
  int timerperiod;
  unsigned long flags;

  switch (cmd)
  {
    case CLOSE_TIMER_CMD: /* 关闭定时器 */
         del_timer_sync(&strTimer.timer);
         break;
    case OPEN_TIMER_CMD: /* 打开定时器 */
         spin_lock_irqsave(&strTimer.lock, flags);
         //保存中断状态,禁止本地中断,并获取自旋锁;
         timerperiod = strTimer.timeperiod;
         spin_unlock_irqrestore(&strTimer.lock, flags);
         //将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。
         mod_timer(&strTimer.timer, jiffies + msecs_to_jiffies(timerperiod));
         //timer=&strTimer.timer:要修改超时时间(定时值)的定时器;
         //expires=jiffies + msecs_to_jiffies(timerperiod):修改后的超时时间;
         //用于修改定时值,如果定时器还没有激活的话,mod_timer()函数会激活定时器;
         //返回值为0表示调用mod_timer()函数前,该定时器未被激活;
         //返回值为1表示调用mod_timer()函数前,该定时器已被激活;
         break;
    case SET_TIMER_PERIOD_CMD: /* 设置定时器周期 */
         spin_lock_irqsave(&strTimer.lock, flags);
         //保存中断状态,禁止本地中断,并获取自旋锁;
         timerperiod=period;
         strTimer.timeperiod = timerperiod;//更新定时器周期
         spin_unlock_irqrestore(&strTimer.lock, flags);
         //将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。

         mod_timer(&strTimer.timer, jiffies + msecs_to_jiffies(timerperiod));
         //timer=&strTimer.timer:要修改超时时间(定时值)的定时器;
         //expires=jiffies + msecs_to_jiffies(timerperiod):修改后的超时时间;
         //用于修改定时值,如果定时器还没有激活的话,mod_timer()函数会激活定时器;
         //返回值为0表示调用mod_timer()函数前,该定时器未被激活;
         //返回值为1表示调用mod_timer()函数前,该定时器已被激活;
         break;
    default: break;
  }
}

void Delete_Timer(void)
{
  del_timer_sync(&strTimer.timer); /* 删除timer */

#if 0
  del_timer(&strTimer.tiemr);
#endif
}

/* 定时器回调函数 */
void Timer_function(struct timer_list *arg)
{
/* from_timer是个宏,可以根据结构体的成员地址,获取到这个结构体的首地址。
  第一个参数表示结构体,第二个参数表示第一个参数里的一个成员,第三个参数表
  示第二个参数的类型,得到第一个参数的首地址。
*/
  struct Timer_dev *dev = from_timer(dev, arg, timer);
  static int sta = 1;
  int timerperiod;
  unsigned long flags;

  sta = !sta; /* 每次都取反,实现LED灯反转 */
  led_OnOff(sta);

  /* 重启定时器 */
  spin_lock_irqsave(&dev->lock, flags);
  //保存中断状态,禁止本地中断,并获取自旋锁;
  timerperiod = dev->timeperiod;
  spin_unlock_irqrestore(&dev->lock, flags);
  //将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。

  mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod));
  //timer=&dev->timer:要修改超时时间(定时值)的定时器;
  //expires=jiffies + msecs_to_jiffies(timerperiod):修改后的超时时间;
  //用于修改定时值,如果定时器还没有激活的话,mod_timer()函数会激活定时器;
  //返回值为0表示调用mod_timer()函数前,该定时器未被激活;
  //返回值为1表示调用mod_timer()函数前,该定时器已被激活;
}

void Timer_lock_Init(void)
{
  spin_lock_init(&strTimer.lock);
}

void Set_Timer(void)
{
  /* 初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */
  timer_setup(&strTimer.timer, Timer_function, 0);
}

8)、添加“LinuxTimer.h”

#ifndef __LinuxTimer_H
#define __LinuxTimer_H

#include <linux/types.h>
/*
数据类型重命名
使能bool,u8,u16,u32,u64, uint8_t, uint16_t, uint32_t, uint64_t
使能s8,s16,s32,s64,int8_t,int16_t,int32_t,int64_t
*/
#include <linux/spinlock_types.h> //使能spinlock_t结构
#include <linux/timer.h> //使能timer_list结构
#include <asm-generic/ioctl.h> //使能“_IO宏”

#define CLOSE_TIMER_CMD      (_IO(0XEF, 0x1))  /* 关闭定时器 */
#define OPEN_TIMER_CMD       (_IO(0XEF, 0x2))  /* 打开定时器 */
#define SET_TIMER_PERIOD_CMD (_IO(0XEF, 0x3))  /* 设置定时器周期命令*/


struct Timer_dev{
  struct timer_list timer; /* 定义一个定时器 */
  int timeperiod; /* 定时周期,单位为ms */

  spinlock_t lock; /* 定义自旋锁 */
};
extern struct Timer_dev strTimer;

extern void Timeperiod_Init(void);
extern void Timer_Config(unsigned int cmd, unsigned long period);
extern void Delete_Timer(void);
extern void Timer_lock_Init(void);
extern void Set_Timer(void);

#endif

9)、添加“TimerDriver.c”

#include "TimerDriver.h"
#include "LinuxTimer.h"
#include "LED.h"
#include <linux/types.h>
//数据类型重命名
//使能bool,u8,u16,u32,u64, uint8_t, uint16_t, uint32_t, uint64_t
//使能s8,s16,s32,s64,int8_t,int16_t,int32_t,int64_t
#include <linux/ide.h>
//使能copy_from_user(),copy_to_user()
#include <linux/module.h>
//使能TimerDriver_init(),TimerDriver_exit()

#define TimerDriver_CNT    1   //定义设备数量为1
#define TimerDriver_NAME  "TimerDriver"  //定义设备的名字

struct TimerDriver_dev strTimerDriver;

/* 打开设备 */
static int TimerDriver_open(struct inode *inode, struct file *filp)
{
  int ret;

  filp->private_data = &strTimerDriver; /*设置私有数据*/
  Timeperiod_Init();/* 默认周期为1s */

  ret=Get_gpio_led_num();//读引脚编号
  if(ret < 0) return ret; 

/* 1、申请“gpio编号”*/
  ret=led_GPIO_request();//申请“gpio编号” 
  if(ret < 0) return ret;//向gpio子系统申请使用“gpio编号” 失败

  printk("TimerDriver_open!\r\n");
  return 0;
}

/*
filp: 要打开的设备文件(文件描述符)
cmd: 应用程序发送过来的命令
arg: 参数
返回值: 0 成功;其他 失败
*/
static long TimerDriver_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
  Timer_Config(cmd,arg);

  return 0; 
}

/* 从设备读取数据,保存到首地址为buf的数据块中,长度为cnt个字节 */
//file结构指针变量flip表示要打开的设备文件
//buf表示用户数据块的首地址
//cnt表示用户数据的长度,单位为字节
//loff_t结构指针变量offt表示“相对于文件首地址的偏移”
static ssize_t TimerDriver_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
  return 0;
}

/* 向设备写数据,将数据块首地址为buf的数据,长度为cnt个字节,发送给用户 */
//file结构指针变量flip表示要打开的设备文件
//buf表示用户数据块的首地址
//cnt表示用户数据的长度,单位为字节
//loff_t结构指针变量offt表示“相对于文件首地址的偏移”
static ssize_t TimerDriver_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
  return 0;
}

/* 关闭/释放设备 */
static int TimerDriver_release(struct inode *inode, struct file *filp)
{
  Delete_Timer(); /* 删除timer */
  led_OnOff(1);   //关闭LED灯
  MyLED_free();

  /*关闭驱动文件的时候释放原子变量,便于其它线程使用*/
  printk("TimerDriver_release!\r\n");
  return 0;
}

/*声明file_operations结构变量MyCharDevice_fops*/
/*它是指向设备的操作函数集合变量*/
const struct file_operations TimerDriver_fops = {
  .owner = THIS_MODULE,
  .open = TimerDriver_open,
  .unlocked_ioctl = TimerDriver_unlocked_ioctl,
  .read = TimerDriver_read,
  .write = TimerDriver_write,
  .release = TimerDriver_release,
};



/*驱动入口函数 */
static int  __init TimerDriver_init(void)
{
  int ret;

  Timer_lock_Init();
  /* 初始化自旋锁 */

//  ret=Get_gpio_led_num();//读引脚编号
//  if(ret < 0) return ret; 

/* 1、申请“gpio编号”*/
//  ret=led_GPIO_request();//申请“gpio编号” 
//  if(ret < 0) return ret;//向gpio子系统申请使用“gpio编号” 失败

  /*2、申请设备号*/
  strTimerDriver.major=0;
  if(strTimerDriver.major)/*如果指定了主设备号*/
  {
    strTimerDriver.devid = MKDEV(strTimerDriver.major, 0);
    //输入参数strTimerDriver.major为“主设备号”
    //输入参数0为“次设备号”,大部分驱动次设备号都选择0
    //将strTimerDriver.major左移20位,再与0相或,就得到“Linux设备号”
    ret=register_chrdev_region( strTimerDriver.devid,\
                       TimerDriver_CNT, \
                       TimerDriver_NAME );
    //strTimerDriver.devid表示起始设备号
    //TimerDriver_CNT表示次设备号的数量
    //TimerDriver_NAME表示设备名
    if(ret < 0)
      goto free_gpio;
  }
  else
  { /* 没有定义设备号 */
    ret=alloc_chrdev_region( &strTimerDriver.devid,\
                     0, \
                     TimerDriver_CNT,\
                     TimerDriver_NAME);
    /* 申请设备号 */
    //strTimerDriver.devid:保存申请到的设备号
    //0:次设备号的起始地址
    //TimerDriver_CNT:要申请的次设备号数量;
    //TimerDriver_NAME:表示“设备名字”
    if(ret < 0)
      goto free_gpio;

    strTimerDriver.major = MAJOR(strTimerDriver.devid);
    /* 获取分配号的主设备号 */
    //输入参数strTimerDriver.devid为“Linux设备号”
    //将strTimerDriver.devid右移20位得到“主设备号”
    strTimerDriver.minor = MINOR(strTimerDriver.devid);
    /* 获取分配号的次设备号 */
    //输入参数strTimerDriver.devid为“Linux设备号”
    //将strTimerDriver.devid与0xFFFFF相与后得到“次设备号”
  }

  /*3、注册字符设备*/
  strTimerDriver.cdev.owner = THIS_MODULE;
  //使用THIS_MODULE将owner指针指向当前这个模块
  cdev_init(&strTimerDriver.cdev,&TimerDriver_fops);
  //注册字符设备,初始化“字符设备结构变量strTimerDriver.cdev”
  //strTimerDriver.cdev是等待初始化的结构体变量
  //TimerDriver_fops就是字符设备文件操作函数集合

  /*4、添加字符设备*/
  ret=cdev_add(&strTimerDriver.cdev,strTimerDriver.devid,TimerDriver_CNT);
  //添加字符设备
  /*&strTimerDriver.cdev表示指向要添加的字符设备,即字符设备结构strTimerDriver.cdev变量*/
  //strTimerDriver.devid表示设备号
  //TimerDriver_CNT表示需要添加的设备数量
  if(ret < 0 ) //添加字符设备失败
    goto del_register;

  printk("dev id major = %d,minor = %d\r\n", strTimerDriver.major, strTimerDriver.minor);
  printk("TimerDriver_init is ok!!!\r\n");

  /*5、自动创建设备节点 */
  strTimerDriver.class =class_create(THIS_MODULE, TimerDriver_NAME);
  if (IS_ERR(strTimerDriver.class)){
    goto del_cdev;
  }

  /*6、创建设备 */
  strTimerDriver.device = device_create(strTimerDriver.class, NULL, strTimerDriver.devid, NULL, TimerDriver_NAME);
  //创建设备
  //设备要创建在strTimerDriver.class类下面
  //NULL表示没有父设备
  //strTimerDriver.devid是设备号;
  //参数drvdata=NULL,设备没有使用数据
  //TimerDriver_NAME是设备名字
  //如果设置fmt=TimerDriver_NAME 的话,就会生成/dev/TimerDriver_NAME设备文件。
  //返回值就是创建好的设备。
  if (IS_ERR(strTimerDriver.device)){
    goto destroy_class;
  }

  /* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */
  Set_Timer();

  return 0;

destroy_class:
  class_destroy(strTimerDriver.class);
  //删除类
  //strTimerDriver.class就是要删除的类

del_cdev:
   cdev_del(&strTimerDriver.cdev);
   //删除字符设备
   //&strTimerDriver.cdev表示指向需要删除的字符设备,即字符设备结构strTimerDriver.cdev变量

del_register:
  unregister_chrdev_region(strTimerDriver.devid, TimerDriver_CNT);
  /* 释放设备号 */
  //strTimerDriver.devid:需要释放的起始设备号
  //TimerDriver_CNT:需要释放的次设备号数量;

free_gpio://申请设备号失败
  /*释放gpio编号*/
  //MyLED_free();
  return -EIO;
}

/*驱动出口函数 */
static void __exit TimerDriver_exit(void)
{
  Delete_Timer(); /* 删除timer */

  /*1、删除字符设备*/
  cdev_del(&strTimerDriver.cdev);
  /*删除字符设备*/
  /*&strTimerDriver.cdev表示指向需要删除的字符设备,即字符设备结构&strTimerDriver.cdev变量*/

  /*2、 释放设备号 */
  unregister_chrdev_region(strTimerDriver.devid, TimerDriver_CNT);
  /*释放设备号 */
  //strTimerDriver.devid:需要释放的起始设备号
  //TimerDriver_CNT:需要释放的次设备号数;

  /*3、 删除设备 */
  device_destroy(strTimerDriver.class, strTimerDriver.devid);
  //删除创建的设备
  //strTimerDriver.class是要删除的设备所处的类
  //strTimerDriver.devid是要删除的设备号
  
  /*4、删除类*/
  class_destroy(strTimerDriver.class);
  //删除类
  //strTimerDriver.class就是要删除的类

  /*5、释放gpio编号*/
  //MyLED_free();
}

module_init(TimerDriver_init);
//指定TimerDriver_init()为驱动入口函数
module_exit(TimerDriver_exit); 
//指定TimerDriver_exit()为驱动出口函数

MODULE_AUTHOR("Zhanggong");//添加作者名字
MODULE_LICENSE("GPL");//LICENSE采用“GPL协议”
MODULE_INFO(intree,"Y");
//去除显示“loading out-of-tree module taints kernel.”

10)、添加“TimerDriver.h”

#ifndef __TimerDriver_H
#define __TimerDriver_H

#include <linux/types.h>
/*
数据类型重命名
使能bool,u8,u16,u32,u64, uint8_t, uint16_t, uint32_t, uint64_t
使能s8,s16,s32,s64,int8_t,int16_t,int32_t,int64_t
*/
#include <linux/cdev.h> //使能cdev结构
#include <linux/cdev.h> //使能class结构和device结构
#include <linux/spinlock_types.h> //使能spinlock_t结构

//#define CLOSE_TIMER_CMD      (_IO(0XEF, 0x1))  /* 关闭定时器 */
//#define OPEN_TIMER_CMD       (_IO(0XEF, 0x2))  /* 打开定时器 */
//#define SET_TIMER_PERIOD_CMD (_IO(0XEF, 0x3))  /* 设置定时器周期命令*/

struct TimerDriver_dev{
  dev_t devid; /*声明32位变量devid用来给保存设备号*/
  int major;   /*主设备号*/
  int minor;   /*次设备号*/
  struct cdev  cdev; /*字符设备结构变量cdev */

  struct class *class;     /*类*/
  struct device *device;  /*设备*/
};
extern struct TimerDriver_dev strTimerDriver;

#endif

11)、添加“Timer_APP.c”

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <sys/ioctl.h>

//APP运行命令: ./Timer_APP /dev/TimerDriver
#define CLOSE_TIMER_CMD      (_IO(0XEF, 0x1))  /* 关闭定时器 */
#define OPEN_TIMER_CMD       (_IO(0XEF, 0x2))  /* 打开定时器 */
#define SET_TIMER_PERIOD_CMD (_IO(0XEF, 0x3))  /* 设置定时器周期命令*/

/*
参数argc: argv[]数组元素个数
参数argv[]:是一个指针数组
返回值: 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
  int fd, ret;
  char *filename;
  unsigned int cmd;
  unsigned int arg;
  unsigned char str[100];

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

  //argv[]是指向输入参数./Timer_APP /dev/TimerDriver
  filename = argv[1];
  //argv[1]指向字符串“/dev/LED”

  fd = open(filename, O_RDWR);
  //如果打开“/dev/LED”文件成功,则fd为“文件描述符”
  //fd=0表示标准输入流; fd=1表示标准输出流;fd=2表示错误输出流;
  if(fd < 0)
  {
    printf("Can't open file %s\r\n", filename);
    return -1;
  }

  while (1)
  {
    printf("Input CMD:");
    ret = scanf("%d", &cmd);
    if (ret != 1)
    { /* 参数输入错误 */
      fgets(str, sizeof(str), stdin); /* 防止卡死 */
    }

    if(4 == cmd) /* 退出APP */
      goto out;

    if(cmd == 1) /* 关闭LED灯 */
      cmd = CLOSE_TIMER_CMD;
    else if(cmd == 2) /* 打开LED灯 */
      cmd = OPEN_TIMER_CMD;
    else if(cmd == 3)
    {
      cmd = SET_TIMER_PERIOD_CMD; /* 设置周期值 */
      printf("Input Timer Period:");
      ret = scanf("%d", &arg);
      if (ret != 1)
      { /* 参数输入错误 */
        fgets(str, sizeof(str), stdin); /* 防止卡死 */
      }
    }
    ioctl(fd, cmd, arg); /* 控制定时器的打开和关闭 */
  }

out:
  ret = close(fd);
  /* 关闭设备 */
  //fd表示要关闭的“文件描述符”
  //返回值等于0表示关闭成功
  //返回值小于0表示关闭失败
  if(ret < 0)
  {
    printf("Can't close file %s\r\n", filename);
    return -1;
  }

  return 0;
}

12)、添加“Makefile”

KERNELDIR := /home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31
#使用“:=”将其后面的字符串赋值给KERNELDIR
CURRENT_PATH := $(shell pwd)
#采用“shell pwd”获取当前打开的路径
#使用“$(变量名)”引用“变量的值”
MyAPP := Timer_APP
MyTimer-objs = TimerDriver.o LinuxTimer.o LED.o
obj-m := MyTimer.o

CC := arm-none-linux-gnueabihf-gcc

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

app:
	$(CC)  $(MyAPP).c  -o $(MyAPP)

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
	rm $(MyAPP)

install:
	sudo cp *.ko $(MyAPP) /home/zgq/linux/nfs/rootfs/lib/modules/5.4.31/ -f

13)、添加“c_cpp_properties.json

按下“Ctrl+Shift+P”,打开VSCode控制台,然后输入“C/C++:Edit Configurations(JSON)”,打开以后会自动在“.vscode ”目录下生成一个名为“c_cpp_properties.json” 的文件。

修改c_cpp_properties.json内容如下所示:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31",
                "/home/zgq/linux/Linux_Drivers/MyTimer", 
                "/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31/arch/arm/include",
                "/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31/include",
                "/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31/arch/arm/include/generated"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "gnu11",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "gcc-x64"
        }
    ],
    "version": 4
}

14)、编译

输入“make clean回车

输入“make drv回车

输入“make app回车

输入“make install回车

输入“ls /home/zgq/linux/nfs/rootfs/lib/modules/5.4.31/ -l回车”查看是存在“Timer_APP和MyTimer.ko

15)、测试

启动开发板,从网络下载程序

输入“root

输入“cd /lib/modules/5.4.31/回车

切换到“/lib/modules/5.4.31/”目录

注意:“lib/modules/5.4.31/在虚拟机中是位于“/home/zgq/linux/nfs/rootfs/”目录下,但在开发板中,却是位于根目录中

输入“ls -l”查看“MyTimer.ko和Timer_APP”是否存在

输入“depmod”,驱动在第一次执行时,需要运行“depmod”

输入“modprobe MyTimer.ko”,加载“MyTimer.ko”模块

输入“lsmod”查看有哪些驱动在工作

输入“ls /dev/TimerDriver -l回车”,发现节点文件“/dev/TimerDriver

输入“./Timer_APP /dev/TimerDriver回车”,APP运行

输入2回车,LED按照1秒亮1秒灭闪烁

输入3回车,再输入100,LED按照100毫秒亮100毫秒灭闪烁

输入1回车,关闭定时器

输入4回车,关闭APP

输入“rmmod MyTimer.ko”,卸载“MyTimer.ko”模块

注意:输入“rmmod MyTimer”也可以卸载“MyTimer.ko”模块

输入“lsmod”查看有哪些驱动在工作。

输入“ls /dev/Timer_APP -l回车”,查询节点文件“/dev/Timer_APP”是否存在

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/492366.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

鸿蒙HarmonyOS应用开发—AbilityStage组件容器

AbilityStage是一个Module级别的组件容器&#xff0c;应用的HAP在首次加载时会创建一个AbilityStage实例&#xff0c;可以对该Module进行初始化等操作。 AbilityStage与Module一一对应&#xff0c;即一个Module拥有一个AbilityStage。 DevEco Studio默认工程中未自动生成Abil…

HWM豪迈电子听漏仪维修相关仪XMIC-lite DXmic-lite

漏水检测系列仪器维修Leakage ManagemHWM豪迈电子听漏仪维修XMIC-lite DXmic-liteent检测仪器包括&#xff1a;Xmic电子听漏仪维修、Xmic-lite电子听漏仪&#xff1b;DXmic&#xff1b;DXmic-lite等系列。也叫电子地面麦克风,用来放大水从带有压力的自来水管道中泄漏出来产生的…

小黑的Vue前端之路:js中通过构造函数封装,设置对象getter和setter方法

js中构造函数创建对象 JavaScript本身并不是设计成面向对象的,所以没有class之类的关键字用来定义类,但JavaScript本身相当灵活,可以利用function关键字来定义类并创建对象。 例如js创建person对象 通过new关键字&#xff0c;把函数当成了创建对象的构造函数 function Pers…

STM32学习笔记(6_7)- TIM定时器的编码器接口原理

无人问津也好&#xff0c;技不如人也罢&#xff0c;都应静下心来&#xff0c;去做该做的事。 最近在学STM32&#xff0c;所以也开贴记录一下主要内容&#xff0c;省的过目即忘。视频教程为江科大&#xff08;改名江协科技&#xff09;&#xff0c;网站jiangxiekeji.com 现在开…

如何在数字化转型中确保数据安全

随着科技的飞速发展&#xff0c;数字化转型已成为企业发展的必然趋势。数字化转型是指企业利用数字技术对业务流程、组织结构和商业模式进行全面创新和变革&#xff0c;以提高企业的竞争力和创新能力。然而&#xff0c;在数字化转型过程中&#xff0c;数据安全问题日益凸显&…

【C语言_函数_复习篇】

目录 一、C语言中函数的概念 二、库函数 2.1 库函数的概念 2.2 怎样自己学习库函数_以sqrt函数为例 三、自定义函数 3.1 自定义函数的语法形式 3.2 自定义函数的举例 3.3 自定义函数的实参 3.4 自定义函数的形参 3.5 自定义函数的实参和形参的关系 3.6 自定义函数中的return…

javaweb-配置优先级、bean管理和SpringBoot原理

javaweb原理&#xff01; 配置优先级 命令行参数的优先级要高于java系统属性 Bean管理 第三方bean SpringBoot原理 起步依赖的原理就是依赖传递 后端开发总结 原来SSM是这三个&#xff01;&#xff01;&#xff01; Maven高级 maven构建和管理ja…

浅谈电商网络爬虫技术

摘 要 目前网络上存在着海量的数据资料&#xff0c;将这些数据爬取保存下来&#xff0c;并进行进一步操作&#xff0c;即可挖掘出数据的潜在价值。如今的互联网存在的缺陷是用户很难获得有用的数据资料&#xff0c;虽然传统的搜索引擎可以为用户返回大量信息&#xff0c;但是…

会话式AI定制化营销的力量

智能革命&#xff1a;如何用会话式AI提高品牌影响力&#xff1f; 在人工智能时代&#xff0c;营销活动计划应充分利用AI技术&#xff0c;比如数据分析、个性化推荐、智能客服等&#xff0c;以提高营销活动的针对性和效率。同时&#xff0c;创意和用户体验依然是吸引用户的关键&…

【Linux实践室】Linux用户管理实战指南:用户密码管理操作详解

&#x1f308;个人主页&#xff1a;聆风吟_ &#x1f525;系列专栏&#xff1a;Linux实践室、网络奇遇记 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 一. ⛳️任务描述二. ⛳️相关知识2.1 &#x1f514;用户密码存放地及方式2.2 &#x1f514;使用…

【数据分享】1929-2023年全球站点的逐日平均海平面压力(Shp\Excel\免费获取)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、能见度等指标&#xff0c;说到气象数据&#xff0c;最详细的气象数据是具体到气象监测站点的数据&#xff01; 有关气象指标的监测站点数据&#xff0c;之前我们分享过1929-2023年全球气象站…

solr-lucene的解释

什么是solr-lucene Solr是一个基于Apache Lucene的开源搜索平台。Lucene是一个开源的全文搜索引擎库&#xff0c;它提供了一个简单而强大的API&#xff0c;用于索引和搜索文档。Solr通过在Lucene的基础上构建一个更高级的搜索平台&#xff0c;为用户提供了更丰富的功能和易于使…

代码随想录——在排序数组中查找元素的第一个和最后一个位置(Leetcode34)需要回顾

题目链接 class Solution {public int[] searchRange(int[] nums, int target) {// count记录数组中与target相等的个数int count 0;// index记录最后一个与target相等的数组下标&#xff0c;先赋值-1证明没有与之相等的数组元素int index -1;// 返回数组arrint[] arr new…

行业分析:2024年全球CRM九大发展趋势预测,洞悉未来!

2024年全球CRM行业发展九大预测&#xff1a;以客户为中心、重视CRM采用率、与业务深度融合的AI、一体化平台、行业垂类解决方案CRM、集中数据&#xff0c;驱动业务、低代码PaaS、在客户服务中寻求人机平衡、不同规模企业应用特点区别明显。 每到年初&#xff0c;作为Gartner魔力…

Netty学习——源码篇7 Pipeline的事件传播机制1 备份

上篇&#xff1a;Netty学习——源码篇6 Pipeline设计原理 已经知道AbstractChannelHandlerContext中有Inbound和Outbound两个boolean变量&#xff0c;分别用于识别Context所对应的Handler的类型。 1、Inbound为true时&#xff0c;表示其对应的ChannelHandler是ChannelInboundHa…

考了PMP证后工资大概是多少 ?

PMP自1999年引入国内以来&#xff0c;大家对这个证书的了解并不深&#xff0c;每年考试的人数也不多。但随着越来越多的企业认可PMP认证&#xff0c;目前考证的人数不断增加&#xff0c;几乎所有与项目管理相关的人都知道这个证书的重要性。这个证书在招聘要求中出现频率较高&a…

小红的炸砖块

题目描述 小红正在玩一个“炸砖块”游戏&#xff0c;游戏的规则如下&#xff1a; 初始有一个n∗m的砖块矩阵。小红会炸k次&#xff0c;每次会向一个位置投炸弹&#xff0c;如果这个位置有一个砖块&#xff0c;则砖块消失&#xff0c;上方的砖块向下落。 小红希望你画出最终砖块…

StringBuffer和大数值

读取 import java.util.Scanner;public class index{public static void main(String[] args){Scanner in new Scanner(System.in);System.out.println("Whats your name?");String name in.nextLine();Scanner inage new Scanner(System.in);System.out.printl…

Linux虚拟机环境搭建spark

Linux环境搭建Spark分为两个版本&#xff0c;分别是Scala版本和Python版本。 一、 安装Pyspark 本环境以 Python 环境为例。 1、下载spark 下载网址&#xff1a;https://archive.apache.org/dist/spark 下载安装包&#xff1a;根据自己环境选择合适版本&#xff0c;本环境…

详细分析Linux中的core dump异常(附 Demo排查)

目录 1. 基本知识2. 进阶知识3. Demo4. 彩蛋 1. 基本知识 Core dump 是指在程序异常终止时&#xff0c;操作系统将程序的内存映像保存到磁盘上的一种机制。 在 Linux 系统中&#xff0c;core dump 提供了一种调试程序错误的重要方式&#xff0c;它记录了程序在崩溃时的内存状态…