【Linux驱动】字符设备驱动模板(五)—— 寄存器驱动LED

前面已经实现了设备号、字符设备的注册以及节点的自动创建,接下来将实际操作外设,下面将直接通过寄存器操作LED,后续还可以通过设备树来操作。

一、驱动入口函数

一般对外设的初始化,只要执行一次即可,所以放在驱动入口函数 xxx_init

1、建立物理地址和虚拟地址的映射

在裸机开发时,因为没有OS,所以一般通过直接读写物理地址来操作寄存器;有了OS后,每个进程都会被分配一个虚拟地址空间,使每个进程在自己的地址空间中运行,防止不同进程随意修改或篡改物理地址的内容

因此,这里需要先通过物理地址拿到对应的虚拟地址,然后再通过操作虚拟地址来驱动外设。建立映射使用 ioremap 函数,函数声明如下: 

/** 建立物理地址和虚拟地址的映射
 * @param cookie 物理地址
 * @param size   物理地址的长度 
 * @return       虚拟地址
 */
#define ioremap(cookie,size)		__arm_ioremap((cookie), (size), MT_DEVICE)

extern void __iomem *__arm_ioremap(phys_addr_t, size_t, unsigned int);

具体操作如下: 

/* 寄存器物理地址 */
#define CCM_CCGR1_BASE							0x20C406C			// 时钟源
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03_BASE	0x20E0068			// IO 复用
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03_BASE	0x20E02F4			// 复用引脚初始化
#define GPIO1_GDIR_BASE							0x209C004			// 输出方向
#define GPIO1_DR_BASE							0x209C000			// LED输出引脚

/* 寄存器虚拟地址 */
static void __iomem* CCM_CCGR1;
static void __iomem* IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
static void __iomem* IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03;
static void __iomem* GPIO1_GDIR;
static void __iomem* GPIO1_DR;

/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
    // ... 

	/* 建立物理地址和虚拟地址的映射 */
	CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = ioremap(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03_BASE, 4);
	IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 = ioremap(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);

    // ... 
}

2、读写虚拟地址

Linux内核提供了虚拟地址的读写函数 readl / writel。函数声明如下: 

/** 从起始虚拟地址开始,读取32bit的内容
 * @param addr   虚拟地址
 * @return       返回读取的32bit的内容
 */
static inline u32 readl(const volatile void __iomem *addr);

/** 向虚拟地址写入内容
 * @param value  要写入的内容
 * @param addr   虚拟地址
 */
static inline void writel(u32 value, volatile void __iomem *addr);

具体操作如下: 

static int __init chrdevbase_init(void)
{
    /* 建立物理地址和虚拟地址的映射 */

	/* GPIO1 时钟源初始化 */
	val = readl(CCM_CCGR1);
	val |= (3 << 26);
	writel(val, CCM_CCGR1);

	/* IO复用 */
	writel(5, IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03);

	/* IO复用引脚初始化 */
	writel(0x10B0, IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03);
	
	/* GPIO1_IO03 输出使能 */
	val = readl(GPIO1_GDIR);
	val |= (1 << 3);
	writel(val, GPIO1_GDIR);

	/* LED 熄灭(GPIO1的第3个引脚为高电平) */
	val = readl(GPIO1_DR);
	val |= (1 << 3);
	writel(val, GPIO1_DR);

    /* 注册设备号、初始化字符设备 */
} 

二、设备操作函数

1、open 操作

当上层应用层调用 open 函数打开文件,获取文件描述符时,内核会调用驱动中对应的 open 函数。pfile->private_data 用于在当前内核模块的文件操作函数之间传递数据,相比于直接使用全局变量,有如下好处:

  • 避免了多线程环境下对全局变量的并发访问,保证了线程安全性
  • 不污染全局命名空间,使用 pfile->private_data 将数据与特定的文件结构体关联起来,使得数据的作用域仅限于该文件
  • 防止其他内核模块访问当前模块中的全局变量,使用 pfile->private_data 能够隐藏实现细节
/*
 * @description 	: 打开设备
 * @param – pinode 	: 传递给驱动的 inode
 * @param - pfile 	: 设备文件,file 结构体有个叫做 private_data 的成员变量
 * 一般在 open 的时候将 private_data 指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_open(struct inode *pinode, struct file *pfile)
{
	printk("open chrdevbase\n");
    
    // 后续其他文件操作函数可直接通过pfile获取,而无需通过全局变量
	pfile->private_data = &chrdev_led;        
    return 0;
}

2、write 操作

在实现 write 操作时,参数 buf 是用户缓冲区,我们需要先将用户缓冲区的数据拷贝到内核缓冲区,其目的如下: 

  • 内核空间和用户空间的隔离。内核空间是受保护的,用户空间的数据不能直接在内核空间上进行操作,为了确保内核的安全性,用户空间的数据必须经过合法性检查
  • 内核空间的访问权限。内核空间拥有对用户空间的访问权限,但是用户空间不能直接访问内核空间,在内核空间需要操作用户空间的数据时,需要通过合法的方法进行访问

将用户数据拷贝到内核缓冲区,调用的 API 为 copy_from_user

enum LED_STAT {
	LED_ON,
	LED_OFF
};

static void led_on(void)
{
	val = readl(GPIO1_DR);
	val &= ~(1 << 3);
	writel(val, GPIO1_DR);
}

static void led_off(void)
{
	val = readl(GPIO1_DR);
	val |= (1 << 3);
	writel(val, GPIO1_DR);
}

/*
 * @description 	: 向设备写数据
 * @param - pfile	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 要给设备写入的数据(用户缓冲区)
 * @param - cnt 	: 要写入的数据长度
 * @param - offset	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t chrdevbase_write(struct file *pfile, const char __user *buf, size_t cnt, loff_t *offset)
{
    // 获取模块数据
	struct chrdev_led_t* pdev = pfile->private_data;

	printk("write chrdevbase\n");
	unsigned char databuf[1];
	unsigned char ledstat;
	
	// 将数据从用户缓冲区拷贝到内核缓冲区
	int ret = copy_from_user(databuf, buf, cnt);
	if(ret != 0)
		return 0;

	ledstat = buf[0] - '0';
	if (ledstat == LED_ON)
	{
		led_on();
	}
	else if(ledstat == LED_OFF)
	{
		led_off();
	}
    return cnt;
}

3、read 操作

和write 操作同理,如果需要将内核数据拷贝到用户缓冲区,同样需要通过 API 来实现,这里使用的API 为 copy_to_user

/*
 * @description 	: 从设备读取数据
 * @param - pfile	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 缓冲区长度
 * @param - offset	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t chrdevbase_read(struct file *pfile, char __user *buf, size_t cnt, loff_t *offset)
{
    /* 用户实现具体功能 */
	struct chrdev_led_t* pdev = pfile->private_data;
	
	const char* msg = "hello, user";
	int ret = copy_to_user(buf, msg, cnt);
	if(ret == 0)
	{
		printk("kernel send data ok!\n");
	}
	else
	{
		printk("kernel send data failed!\n");
	}

    return 0;
}

三、测试用例

因为本人使用的板子,在上电时内核初始化了中断,每隔一段时间就会自动进入到中断切换 LED 的状态,所以这里将通过 while 循环让 LED 常亮 / 常灭(一般执行一次就行,这里因为有中断才这么做)

命令的基本格式为:  ./ 

具体测试用例的实现如下: 

#include 
#include 
#include 
#include 
#include 
#include 

void printHelp()
{
    printf("usage: ./chrdevbaseApp  \n");
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        printHelp();
        return -1;
    }
    
    char* driver_path = argv[1];       // 位置0 保存的是 ./chrdevbaseApp
    char* led_stat = argv[2];

    int fd = open(driver_path, O_WRONLY);
    if (fd < 0)
    {
        perror("open file failed");
        return -2;
    }


    while (1)
    {
        write(fd, led_stat, 1);
    }
    
    close(fd);
    return 0;
}

假设生成的执行文件名为 chrdevbaseApp,自动创建的驱动节点路径 driver_path 为 /dev/chrdevbase

输入命令 ./chrdevbaseApp  /dev/chrdevbase 0,LED常亮

你可能感兴趣的:(stm32,嵌入式硬件,单片机)