韦东山嵌入式linux系列-LED驱动程序

之前学习STM32F103C8T6的时候,学习过对应GPIO的输出:

操作STM32的GPIO需要3个步骤: 使用RCC开启GPIO的时钟、使用GPIO_Init函数初始化GPIO、使用输入/输出函数控制GPIO口。

【STM32】GPIO输出-CSDN博客

这里再看看STM32MP157的GPIO引脚使用

1 普适的 GPIO 引脚操作方法

GPIO: General-purpose input/output,通用的输入输出口

1.1 GPIO 模块一般结构

⚫ 有多组 GPIO,每组有多个 GPIO
⚫ 使能:电源/时钟
⚫ 模式(Mode):引脚可用于 GPIO 或其他功能
⚫ 方向:引脚 Mode 设置为 GPIO 时,可以继续设置它是输出引脚,还是输入引脚
⚫ 数值:
◼ 对于输出引脚,可以设置寄存器让它输出高、低电平
◼ 对于输入引脚,可以读取寄存器得到引脚的当前电平

1.2 GPIO 寄存器操作

⚫ 芯片手册一般有相关章节,用来介绍: power/clock
        ◼ 可以设置对应寄存器使能某个 GPIO 模块(Module)
        ◼ 有些芯片的 GPIO 是没有使能开关的,即它总是使能的
⚫ 一个引脚可以用于 GPIO、串口、 USB 或其他功能,
        ◼ 有对应的寄存器来选择引脚的功能
⚫ 对于已经设置为 GPIO 功能的引脚,有方向寄存器用来设置它的方向:输出、输入
⚫ 对于已经设置为 GPIO 功能的引脚,有数据寄存器用来写、读引脚电平状态
GPIO 寄存器的 2 种操作方法: 原则:不能影响到其他位

① 直接读写:读出、修改对应位、写入
        a) 要设置 bit n:

val = data_reg;            // 读出来
val = val | (1<<n);        // 1左移n位,再或
data_reg = val;            // 写回去

b) 要清除 bit n:

val = data_reg;
val = val & ~(1<<n);
data_reg = val;

② set-and-clear protocol:
set_reg, clr_reg, data_reg 三个寄存器对应的是同一个物理寄存器,
a) 要设置 bit n: set_reg = (1<<n);
b) 要清除 bit n: clr_reg = (1<<n);

2 STM32MP157 GPIO 操作方法

RCC: Reset and clock control (复位和时钟控制)
GPIO: General-purpose input/output,通用的输入输出口
如下图所示,我们需要操作 LED2 引脚标号为 PA10 LED3 引脚标号为 PG8

2.1 STM32MP157 的 GPIO 模块结构

参 考 资 料 : CPU 开 发 手 册 DM00327695.pdf《 13: General-purposeI/Os(GPIO)》。

GPIO 寄存器有多种,包括功能寄存器和 GPIO 时钟寄存器,下面进行具体介绍,如下为引脚对应的表。
GPIOA (16 pins) <--对应--> gpiochip0
GPIOB (16 pins) <--对应--> gpiochip1
GPIOC (16 pins) <--对应--> gpiochip2
GPIOD (16 pins) <--对应--> gpiochip3
GPIOE (16 pins) <--对应--> gpiochip4
GPIOF (16 pins) <--对应--> gpiochip5
GPIOG (16 pins) <--对应--> gpiochip6
GPIOH (16 pins) <--对应--> gpiochip7
GPIOI (16 pins) <--对应--> gpiochip8
....
GPIOZ (8 pins) <--对应--> gpiochip9

由于 GPIO 的功能多种多样,我们需要根据实际功能来设置 GPIO 引脚的工作模式。对于 STM32MP157 来说,每一个 GPIO 端口有四个32位的配置寄存器( GPIOx_MODER, GPIOx_OTYPER, GPIOx_OSPEEDR 和 GPIOx_PUPDR),两个32 位的数据寄存器(GPIOx_IDR 和 GPIOx_ODR),一个 32 位的设置/复位寄存器 (GPIOx_BSRR) 。 此 外 , 所 有 的 GPIO 都 有 一 个 32 位 的 锁 定 寄 存 器( GPIOx_LCKR ) 和 两 个 32 位 的 多 功 能 选 择 寄 存 器 ( GPIOx_AFRH and GPIOx_AFRL)。此外,还有 GPIO 外设时钟控制寄存器。

通过编程寄存器,我们可以设置 GPIO 为不同的模式,包括以下几种。
⚫ 输入悬空
⚫ 输入上拉
⚫ 输入下拉
⚫ 模拟输入
⚫ OD 输出,支持上拉或者下拉
⚫ Push-pull 输出,支持上拉或者下拉
⚫ 多功能 push-pull 输出,支持上拉或者下拉
⚫ 多功能 OD 输出,支持上拉或者下拉

下图为 GPIO 的基本结构简图,可以看到支持的各种不同模式和对应的寄存器,我们需要根据实际的功能来设置输入输出功能。

和STM32F108C8T6一样

参考:【STM32】GPIO输出-CSDN博客

每 个 GPIO 端 口 的 基 地 址 不 同 , 具 体 的 对 应 可 以 通 过 查 看 参考手册《dm00327659.pdf》的 memory map 章节来查看,下面红框内是本次实验使用到的 GPIOA 和 GPIOG 的基地址

GPIO 的 使 用 涉 及 到 多 个 寄 存 器 , 包 括 RCC 时 钟 GPIOx_MODER 、GPIOx_IDR 和 GPIOx_ODR 等。

2.2 RCC 用于设置是否向 GPIO 模块提供时钟

数据手册截图

GPIO 外设的时钟来源各自不同,具体的可以从参考手册当中可以看到,详见《dm00327659.pdf》 RCC 章节 Table56。下图为截图,其中 GPIOA-K 的时钟来源为 hclk4, GPIOZ 的时钟来源为 hclk5。因此为了使用 GPIO,我们需要使能锁相环和外设 GPIO 各自对应的时钟。

接下来我们要找到配置锁相环 PLL4 时钟源的寄存器,我们需要将其使能,并等待完成锁定。查看 CPU 开发手册 DM00327695.pdf《10 Reset and clock control (RCC)》

由于 STM32MP157 包含 cortex-m4 和 cortex-A7 两部分,并且 GPIO 可以分别由 MCU 和 APU 来控制,因此 GPIO 的时钟使能也分成两种。

2.3 GPIOx_MODER 配置 GPIO 模式

GPIOx_MODER 用于配置 GPIO 的模式,包括输入、通用输出、多功能和模拟共四种模式。该寄存器共 32 位,,涉及 16 个 GPIO,每个 GPIO 对应 2 位。GPIOx_MODER 的各位定义如下,我们在这里分别选择 00 和 01 两种,各自对应输入和输出模式。(上电默认为输入悬空模式)。其中 00 对应输入功能, 01 对应输出功能。

我们需要设置 PA10 对应 GPIOA10 其中 GPIOA_MODER 寄存器基地址为0x50002000 由 于 每 个 GOIO 引 脚 对 应 两 个 bit 位, 所 以 我们需要操 作bit[21:20]设置为 01 输出模式


PG8 对应 GPIOG8 其中 GPIOA_MODER 寄存器基地址为 0x50008000 由于每个 GOIO 引脚对应两个 bit 位,所以我们需要操作 bit[17:16]设置为 01 输出模式

2.4 GPIOx_OTYPER 配置 GPIO 输出

GPIOx_OTYPER 寄存器用于配置输出类型,可以设置为输出为 OD 或者push-pull 两种结构。该寄存器共 32 位,低 16 位对应 16 个 GPIO,通过设置位的高低来选择 OD 或者 push-pull 的输出类型

3 GPIOx_IDR 设置输入 GPIO

GPIOx_IDR 是 GPIO 输入数据寄存器,用于存储外部输入的数据。可以通过读取该寄存器的值来判断外部输入电平的高低。具体的寄存器定义如下图所示,只使用低 16 位

3.1 读GPIO

1.设置 RCC_PLL4CR 使能 hclk4 使用的时钟。
2.设置 RCC_MP_AHB4ENSETR 使能 GPIOA 外设时钟。
3.设置 GPIOA_MODER 中某位为 1,把该引脚设置为输入功能。
4.读 GPIOG_IDR 某位的值为 1 或者 0 来判断输入是否电平。

/* 使能 PLL4 */
/* RCC_PLL4CR 地址: 0x50000000 + 0x894 */
pReg = (volatile unsigned int*)(0x50000000 + 0x894);
*pReg |= (1<<0);
while(*pReg & (1 << 1) == 0);
/* for A7 (0x50000000 + 0xA28) */
gpio_clk = (volatile unsigned int*)(0x50000000 + 0xA28);
*gpio_clk |= (1 << 6);        // GPIOG 的外设时钟
// GPIOG 相关的寄存器地址
GPIOG_MODER = (volatile unsigned int*)(0x50008000);
GPIOG_ODR = (volatile unsigned int*)(0x50008014);
// 设置 PG8 为输出 , PG2 和 PG3 为输入
val = *GPIOG_MODER;
val &= ~((3 << 4) | (3 << 6));
unsigned int key_val = 0;unsigned int val;
val = *GPIOG_IDR;              // 读取 GPIOG 的输入值
if(( val&0x0004) == 0)         // 如果按键 KEY2, PG2 被按下,则输出 1
{key_val = 1;
}
else if((val &0x0008) == 0)     // 如果按键 KEY1, PG3 被按下,则输出 1
{key_val = 2;
}
else                            // 如果没有按键按下,则输出 0
{key_val = 0;
}
return key_val;                 // 输出检测到的按键值

代码需要研究一下

3.2 写 GPIO

1 设置 RCC_PLL4CR 使能 hclk4 使用的时钟。
2 设置 RCC_MP_AHB4ENSETR 使能 GPIOA 外设时钟。
3 设置 GPIOA_MODER 中某位为 1,把该引脚设置为输出功能。
4 写 GPIOA_ODR 某位的值为 1 或者 0,让其输出高电平或低电平。

/* 使能 PLL4 */
// 锁相环 PLL4 初始
/* RCC_PLL4CR 地址 0x50000000 + 0x894*/
pll_clk = (volatile unsigned int*)(0x50000000 + 0x894);
*pll_clk |= (1 << 0);                // 使能锁相环 PLL4
while(*pll_clk & (1 << 1) == 0);     // 等待锁相环完成锁频
/*GPIOA 和 GPIOG 外设初始化
/* for A7 (0x50000000 + 0xA28) */
gpio_clk = (volatile unsigned int*)(0x50000000 + 0xA28);
*gpio_clk |= (1<<0);            // 使能 GPIOA 的外设时钟
unsigned int val;
// GPIOA 相关的寄存器地址
GPIOA_MODER= (volatile unsigned int*)(0x50002000);
GPIOA_ODR = (volatile unsigned int*)(0x50002014);
// 设置 PA10 为输出
val = *GPIOA_MODER;
val &= ~(3 << 20);
val |= (1 << 20);
*GPIOA_MODER = val;
// 设置 PA10 输出低电平
val = *GPIOA_ODR;
val &= ~(1 << 10);
*GPIOA_ODR = val;

4 LED 驱动程序框架

4.1 回顾字符设备驱动程序框架

图中驱动层访问硬件外设寄存器依靠的是 ioremap 函数去映射到寄存器地址,然后开始控制寄存器。那么该如何编写驱动程序?
① 确定主设备号,也可以让内核分配;
② 定义自己的 file_operations 结构体, 这是核心;
③ 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体;
④ 把 file_operations 结构体告诉内核:通过 register_chrdev 函数;
⑤ 谁来注册驱动程序?需要一个入口函数:安装驱动程序时,就会去调用这个入口函数;
⑥ 有入口函数就应该有出口函数:卸载驱动程序是,出口函数调用unregister_chrdev;
⑦ 其它完善:提供设备信息,自动创建设备节点: class_create,device_create;

4.2 对于 LED 驱动,我们想要什么样的接口?

驱动程序访问硬件,必须先ioremap。

先随便找一个驱动程序,模仿写,作为模版

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/delay.h>
#include <linux/bitrev.h>
#include <linux/mutex.h>
#include <linux/uaccess.h>
#include <linux/io.h>// 主设备号
static int major = 0;
static struct class *led_class;// write函数
static ssize_t led_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos)
{// 从用户拷贝数据char value;copy_from_user(&value, buf, 1);				// 将用户空间buf的数据拷贝1字节到value中// 设置GPIO寄存器1/0if (value){// 设置led on}else{// 设置led off}return 1;
}// open函数
static int led_open(struct inode *inode, struct file *filp)
{// 使能GPIO// 将某个引脚配置成GPIO// 配置GPIO是输出模式,只有用户程序open的时候,才表示要使用这个引脚,这个时候再配置引脚return 0;
}// file_operations结构体
static struct file_operations led_fops = {.owner	= THIS_MODULE,.write	= led_write,.open	= led_open,
};// 入口函数
static int __init led_init(void)
{printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);				// 打印// 注册file_operations结构体,返回值是主设备号major = register_chrdev(0, "winter_led", &led_fops);// 驱动程序访问硬件,必须先ioremap// 创建classled_class= class_create(THIS_MODULE, "myled");// 创建设备device_create(led_class, NULL, MKDEV(major, 0), NULL, "myled");		// 系统会创建名为/dev/myled的设备节点return 0;
}// 出口函数
static void __exit led_exit(void)
{// 卸载设备device_destroy(led_class, MKDEV(major, 0));// 销毁类class_destroy(led_class);// 卸载unregister_chrdev(major, "winter_led");};module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

怎么访问寄存器呢?使用指针。

4.3 简单而led驱动程序

在上面模版的基础上开始完善。

首先根据PPT(文档)设置寄存器:

1.设置 RCC_PLL4CR 使能 hclk4 使用的时钟。
2.设置 RCC_MP_AHB4ENSETR 使能 GPIOA 外设时钟。
3.设置 GPIOA_MODER 中某位为 1,把该引脚设置为输入功能。
4.读 GPIOG_IDR 某位的值为 1 或者 0 来判断输入是否电平。

先定义

// 不能使用物理地址,需要映射
// 1寄存器
// RCC_PLL4CR地址:0x50000000 + 0x894,提供时钟的
static volatile unsigned int* RCC_PLL4CR;// 2使能GPIOA本身
// RCC_MP_AHB4ENSETR地址:0x50000000 + 0xA28
static volatile unsigned int* RCC_MP_AHB4ENSETR;// 3设置引脚为输出模式
// GPIOA_MODER地址:0x50002000 + 0x00,设置bit[21:20]=0b01,用于输出模式
static volatile unsigned int* GPIOA_MODER;// 4设置输出电平
// 方法2:直接写寄存器,一次操作即可,高效
// GPIOA_BSRR地址: 0x50002000 + 0x18
static volatile unsigned int* GPIOA_BSRR;

在init函数中,重映射

// 入口函数
static int __init led_init(void)
{printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);				// 打印// 注册file_operations结构体,返回值是主设备号major = register_chrdev(0, "winter_led", &led_fops);// 驱动程序访问硬件,必须先ioremap,在这里映射,映射的是一页4k的地址,参考// ioremap(base_phy, size);// 1寄存器// RCC_PLL4CR地址:0x50000000 + 0x894,提供时钟的// static volatile unsigned int* RCC_PLL4CR;RCC_PLL4CR = ioremap(0x50000000 + 0x894, 4);// 2使能GPIOA本身// RCC_MP_AHB4ENSETR地址:0x50000000 + 0xA28// static volatile unsigned int* RCC_MP_AHB4ENSETR;RCC_MP_AHB4ENSETR = ioremap(0x50000000 + 0xA28, 4);// 3设置引脚为输出模式// GPIOA_MODER地址:0x50002000 + 0x00,设置bit[21:20]=0b01,用于输出模式// static volatile unsigned int* GPIOA_MODER;GPIOA_MODER = ioremap(0x50002000 + 0x00, 4);// 4设置输出电平// 方法2:直接写寄存器,一次操作即可,高效// GPIOA_BSRR地址: 0x50002000 + 0x18// static volatile unsigned int* GPIOA_BSRR;GPIOA_BSRR = ioremap(0x50002000 + 0x18, 4);// 创建classled_class = class_create(THIS_MODULE, "myled");// 创建设备device_create(led_class, NULL, MKDEV(major, 0), NULL, "myled");		// 系统会创建名为/dev/myled的设备节点return 0;
}

出口函数unremap

// 出口函数
static void __exit led_exit(void)
{iounmap(RCC_PLL4CR);iounmap(RCC_MP_AHB4ENSETR);iounmap(GPIOA_MODER);iounmap(GPIOA_BSRR);// 卸载设备device_destroy(led_class, MKDEV(major, 0));// 销毁类class_destroy(led_class);// 卸载unregister_chrdev(major, "winter_led");};

接着在open函数中:

(1)使能PLL4,是所有GPIO的时钟

1.设置 RCC_PLL4CR 使能 hclk4 使用的时钟。

设置bit0位为1,PLL4 ON

*RCC_PLL4CR |= (1 << 0);					// 设置bit0为1

(2)使能GPIOA

2.设置 RCC_MP_AHB4ENSETR 使能 GPIOA 外设时钟。

// 使能GPIOA
*RCC_MP_AHB4ENSETR |= (1 << 0);				// 1左移0位

(3)将GPIOA的第十个引脚配置成GPIO

3.设置 GPIOA_MODER 中某位为 1,把该引脚设置为输入功能。

// 将GPIOA的第十个引脚配置成GPIO
// 配置GPIO是输出模式,只有用户程序open的时候,才表示要使用这个引脚,这个时候再配置引脚	
*GPIOA_MODER &= ~(3 << 20);					// 清零 11左移20位,取反,
*GPIOA_MODER |= (1 << 20);					// 20位设置成1,配置成01,输出模式

3的二进制是11,左移20位是:11 0000 0000 0000 0000,再取反,和原来的数据&,就将原来20/21位清零;再或上1左移20位,也就,01 0000 0000 0000 0000,

led_open总的代码

// open函数
static int led_open(struct inode *inode, struct file *filp)
{// 使能PLL4,是所有GPIO的时钟*RCC_PLL4CR |= (1 << 0);					// 设置bit0为1while ((*RCC_PLL4CR & (1 << 1)) == 0);		// 如果bit1一直为0的话,就等待// 使能GPIOA*RCC_MP_AHB4ENSETR |= (1 << 0);				// 1左移0位// 将GPIOA的第十个引脚配置成GPIO// 配置GPIO是输出模式,只有用户程序open的时候,才表示要使用这个引脚,这个时候再配置引脚	*GPIOA_MODER &= ~(3 << 20);					// 清零 11左移20位,取反,*GPIOA_MODER |= (1 << 20);					// 20位设置成1,配置成01,输出模式return 0;
}

最后是write函数:将用户空间buf的数据拷贝内核中(对用户来说是写操作,对内核而言是读操作)

4.读 GPIOG_IDR 某位的值为 1 或者 0 来判断输入是否电平。

10和26都对应的是GPIO10,26是1的时候,表示低电平,也就是开启led;10是1的时候,表示高电平,也就是关闭led

// write函数
static ssize_t led_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos)
{// 从用户拷贝数据char value;copy_from_user(&value, buf, 1);				// 将用户空间buf的数据拷贝1字节到value中// 设置GPIOA10寄存器1/0if (value){// 设置led on,让引脚输出低电平*GPIOA_BSRR =  (1 << 26); 				// 1左移26}else{// 设置led off,让引脚输出高电平*GPIOA_BSRR =  (1 << 10); 				// 1左移10}return 1;
}

总结:结合参考手册/数据手册写代码

总的LED驱动程序:led_drv.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/delay.h>
#include <linux/poll.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/uaccess.h>
#include <asm/io.h>
#include <linux/device.h>// 主设备号
static int major = 0;
static struct class *led_class;// 不能使用物理地址,需要映射
// 1寄存器
// RCC_PLL4CR地址:0x50000000 + 0x894,提供时钟的
static volatile unsigned int* RCC_PLL4CR;// 2使能GPIOA本身
// RCC_MP_AHB4ENSETR地址:0x50000000 + 0xA28
static volatile unsigned int* RCC_MP_AHB4ENSETR;// 3设置引脚为输出模式
// GPIOA_MODER地址:0x50002000 + 0x00,设置bit[21:20]=0b01,用于输出模式
static volatile unsigned int* GPIOA_MODER;// 4设置输出电平
// 方法2:直接写寄存器,一次操作即可,高效
// GPIOA_BSRR地址: 0x50002000 + 0x18
static volatile unsigned int* GPIOA_BSRR;// write函数
static ssize_t led_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos)
{// 从用户拷贝数据char value;copy_from_user(&value, buf, 1);				// 将用户空间buf的数据拷贝1字节到value中// 设置GPIOA10寄存器1/0if (value){// 设置led on,让引脚输出低电平*GPIOA_BSRR =  (1 << 26); 				// 1左移26}else{// 设置led off,让引脚输出高电平*GPIOA_BSRR =  (1 << 10); 				// 1左移10}return 1;
}// open函数
static int led_open(struct inode *inode, struct file *filp)
{// 使能PLL4,是所有GPIO的时钟*RCC_PLL4CR |= (1 << 0);					// 设置bit0为1while ((*RCC_PLL4CR & (1 << 1)) == 0);		// 如果bit1一直为0的话,就等待// 使能GPIOA*RCC_MP_AHB4ENSETR |= (1 << 0);				// 1左移0位// 将GPIOA的第十个引脚配置成GPIO// 配置GPIO是输出模式,只有用户程序open的时候,才表示要使用这个引脚,这个时候再配置引脚	*GPIOA_MODER &= ~(3 << 20);					// 清零 11左移20位,取反,*GPIOA_MODER |= (1 << 20);					// 20位设置成1,配置成01,输出模式return 0;
}// file_operations结构体
static struct file_operations led_fops = {.owner	= THIS_MODULE,.write	= led_write,.open	= led_open,
};// 入口函数
static int __init led_drv(void)
{printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);				// 打印// 注册file_operations结构体,返回值是主设备号major = register_chrdev(0, "winter_led", &led_fops);// 驱动程序访问硬件,必须先ioremap,在这里映射,映射的是一页4k的地址,参考// ioremap(base_phy, size);// 1寄存器// RCC_PLL4CR地址:0x50000000 + 0x894,提供时钟的// static volatile unsigned int* RCC_PLL4CR;RCC_PLL4CR = ioremap(0x50000000 + 0x894, 4);// 2使能GPIOA本身// RCC_MP_AHB4ENSETR地址:0x50000000 + 0xA28// static volatile unsigned int* RCC_MP_AHB4ENSETR;RCC_MP_AHB4ENSETR = ioremap(0x50000000 + 0xA28, 4);// 3设置引脚为输出模式// GPIOA_MODER地址:0x50002000 + 0x00,设置bit[21:20]=0b01,用于输出模式// static volatile unsigned int* GPIOA_MODER;GPIOA_MODER = ioremap(0x50002000 + 0x00, 4);// 4设置输出电平// 方法2:直接写寄存器,一次操作即可,高效// GPIOA_BSRR地址: 0x50002000 + 0x18// static volatile unsigned int* GPIOA_BSRR;GPIOA_BSRR = ioremap(0x50002000 + 0x18, 4);// 创建classled_class = class_create(THIS_MODULE, "myled");// 创建设备device_create(led_class, NULL, MKDEV(major, 0), NULL, "myled");		// 系统会创建名为/dev/myled的设备节点return 0;
}// 出口函数
static void __exit led_exit(void)
{iounmap(RCC_PLL4CR);iounmap(RCC_MP_AHB4ENSETR);iounmap(GPIOA_MODER);iounmap(GPIOA_BSRR);// 卸载设备device_destroy(led_class, MKDEV(major, 0));// 销毁类class_destroy(led_class);// 卸载unregister_chrdev(major, "winter_led");};module_init(led_drv);
module_exit(led_exit);
MODULE_LICENSE("GPL");

测试程序:led_test.c

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>// ledtest /dev/myled on 	点灯
// lettest /dev/myled off 	熄灯int main(int argc, char** argv)
{if (argc != 3){printf("Usage: %s <dev> <on/off>\n", argv[0]);printf("eg: %s /dev/myled on\n", argv[0]);printf("eg: %s /dev/myled off\n", argv[0]);return -1;}// openint fd = open(argv[1], O_RDWR);					// 可读可写if (fd < 0){printf("Failed to open %s\n", argv[1]);return -1;}char status = 0;// writeif (strcmp(argv[2], "on") == 0){status = 1;}write(fd, &status, 1);				// 写回去return 0;
}

Makefile

# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH,          比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
#       请参考各开发板的高级用户使用手册KERN_DIR = /home/book/100ask_stm32mp157_pro-sdk/Linux-5.4all:make -C $(KERN_DIR) M=`pwd` modules$(CROSS_COMPILE)gcc -o led_test led_test.cclean:make -C $(KERN_DIR) M=`pwd` modules cleanrm -rf modules.orderrm -f led_testobj-m   += led_drv.o

编译

有bug就照着报错的该就行了

装载驱动程序

insmod led_drv.ko

lsmod

执行测试程序

./led_test /dev/myled on
./led_test /dev/myled off

板子上看不出任何变化,关掉【心跳灯】

ls /sys/class/leds/
echo none > /sys/class/leds/heartbeat/trigger

在执行led on/led off看效果,灯果然灭了。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

逻辑回归中的损失函数

目录 一、损失函数介绍&#xff1a;二、简化上述损失函数&#xff1a; 一、损失函数介绍&#xff1a; 与回归问题成本函数不同的是&#xff0c;逻辑回归模型&#xff08;解决分类问题&#xff09;的成本函数在获得损失J的时候不再用真实值y与预测值y^的差值计算损失&#xff0…

Python面试宝典第11题:最长连续序列

题目 给定一个未排序的整数数组 nums &#xff0c;找出数字连续的最长序列&#xff08;不要求序列元素在原数组中连续&#xff09;的长度。请你设计并实现时间复杂度为 O(n) 的算法解决此问题。 示例 1&#xff1a; 输入&#xff1a;nums [100,4,200,1,3,2] 输出&#xff1a;…

STM32智能电网监控系统教程

目录 引言环境准备智能电网监控系统基础代码实现&#xff1a;实现智能电网监控系统 4.1 数据采集模块 4.2 数据处理与分析 4.3 通信与网络系统实现 4.4 用户界面与数据可视化应用场景&#xff1a;电网监控与优化问题解决方案与优化收尾与总结 1. 引言 智能电网监控系统通过S…

学习网络的第一步:全面解析OSI与TCP/IP模型

我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货! Hello,大家好!我是你们的好朋友小米。今天我们来聊一聊网络基础知识中的重量级选手——OSI模型和TCP/IP模型!网络的世界就像一个巨大的迷宫,而这两个…

肯尼亚PVoC认证

一、肯尼亚PVoC认证介绍 为了向肯尼亚消费者保证&#xff0c;他们购买的进口商品的安全和质量&#xff0c;并保护肯尼亚制造商免受不公平竞争&#xff0c;肯尼亚标准局&#xff08;KEBS&#xff09;是肯尼亚政府的一个法定机构&#xff0c;实施了“出口肯尼亚出口验证&#xff…

【源码开源】C#桌面应用开发:串口调试助手

c#桌面应用开发 1、环境搭建和工程创建&#xff1a;参照番茄定时器项目 工程创建参照 2、界面布局设计 3、具体功能函数 &#xff08;1&#xff09;端口扫描&#xff1a; private void btn_com_scan_Click(object sender, EventArgs e){//端口号扫描ReflashPortToComboBox(…

赤壁之战的烽火台 - 观察者模式

“当烽火连三月&#xff0c;家书抵万金&#xff1b;设计模式得其法&#xff0c;千军如一心。” 在波澜壮阔的三国历史长河中&#xff0c;赤壁之战无疑是一场改变乾坤的重要战役。而在这场战役中&#xff0c;一个看似简单却至关重要的系统发挥了巨大作用——烽火台。这个古老的…

基于ssm的图书管理系统的设计与实现

摘 要 在当今信息技术日新月异的时代背景下&#xff0c;图书管理领域正经历着深刻的变革&#xff0c;传统的管理模式已难以适应现代社会的快节奏和高要求&#xff0c;逐渐向数字化、智能化的方向演进。本论文聚焦于这一转变趋势&#xff0c;致力于设计并成功实现一个基于 SSM&…

在HTTP协议中常见的Token类型

在HTTP协议中&#xff0c;常见的Token类型主要有以下几种&#xff1a; Bearer Token&#xff1a;最常见的类型&#xff0c;用于OAuth 2.0认证&#xff0c;通过Authorization头传递&#xff0c;格式为Bearer <token>。更多请阅读&#xff1a;JWK和JWT 学习-CSDN博客 Basi…

【数据结构】09.树与二叉树

一、树的概念与结构 1.1 树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 根结点&#xff1a;根…

应变与几何方程——弹性力学

变形协调方程 正应变的表达式&#xff1a;切应变的表达&#xff1a; 考虑坐标位移移动造成的增量 应变——考虑物体的变形的剧烈程度 正应变——微元线段长度的变化 剪应变——两微元所夹角度的变化 正应变——拉伸为正&#xff0c;压缩为负 剪应变——夹角减小为正&#x…

删除有序数组中的重复项

26. 删除有序数组中的重复项 - 力扣&#xff08;LeetCode&#xff09; 快慢指针&#xff0c;慢的指针去追赶快的指针&#xff0c;相等时也就是追到时&#xff0c;快指针移动向前 class Solution { public:int removeDuplicates(vector<int>& nums) {int s 1, q 1;i…

sql盲注

文章目录 布尔盲注时间盲注 布尔盲注 介绍&#xff1a;在网页只给你两种回显的时候是用&#xff0c;类似于布尔类型的数据&#xff0c;1表示正确&#xff0c;0表示错误。 特点&#xff1a;思路简单&#xff0c;步骤繁琐且麻烦。 核心函数&#xff1a; length()函数substr()函…

物流智能锁在物流货运智能锁控管理中的应用

一、物流锁控管理的痛点剖析 &#xff08;一&#xff09;货物安全风险高 在传统的物流运输中&#xff0c;常用的机械锁和普通电子锁安全性有限&#xff0c;容易被非法破解或撬开。据不完全统计&#xff0c;每年因货物被盗造成的经济损失高达数十亿。这导致货物在运输途中面临…

前端Canvas入门——怎么用Canvas画一些简单的图案

Canvas作为前端的画图工具&#xff0c;其实用途还是蛮广泛的&#xff0c;但是很多前端学习课程其实都很少涉及到这块内容。 于是乎&#xff0c;就写下这个了。 当然啦&#xff0c;目前还在学习摸索中。 一些实战代码&#xff0c;仅供参考&#xff1a; <canvasid"ctx&…

旅游景区度假村展示型网站如何建设渠道品牌

景区、度假村、境外旅游几乎每天的人流量都非常高&#xff0c;还包括本地附近游等&#xff0c;对景区及度假村等固定高流量场所&#xff0c;品牌和客户赋能都是需要完善的&#xff0c;尤其是信息展示方面&#xff0c;旅游客户了解前往及查看信息等。 通过雨科平台建设景区度假…

C++系列-Vector(一)

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” Vector的介绍及使用 Vector的介绍 当vector构建的参数类型为char类型时&#xff0c;它是和string是极其类似的&#xff0c;但是二者之间也有不同&#xff0c;比如&#xff0c…

旋转矩阵中的易错点

坐标系O1和O2&#xff0c;假设点P在坐标系O1中的坐标是{A1,B1,C1},坐标系O1先沿着y轴旋转-90度&#xff0c;再沿着Z轴旋转45度得到坐标系O2&#xff0c;求该点在坐标系O2中的坐标{A2,B2,C2}。 错误解法&#xff1a; 求出O1到O2的旋转旋转矩阵&#xff1a; 3D Rotation Conve…

Prototype, POC, MVP:区别与比较

在软件开发和产品设计领域&#xff0c;Prototype&#xff08;原型&#xff09;、Proof of Concept&#xff08;概念证明&#xff0c;简称POC&#xff09;和Minimum Viable Product&#xff08;最小可行产品&#xff0c;简称MVP&#xff09;是三个重要的概念。它们各自在项目的不…

在Ubuntu下安装samba实现和Windows系统文件共享

一、安装 apt install -y samba samba-clientSamba is not being run as an AD Domain Controller: Masking samba-ad-dc.service Please ignore the following error about deb-systemd-helper not finding those services. (samba-ad-dc.service masked) Created symlink /et…