嵌入式字符设备驱动——ULN2003步进电机驱动程序实现
之前分享了字符设备驱动程序的实现——hello驱动,是不涉及硬件操作的,我说过要给大家分享一篇涉及硬件操作的字符设备驱动程序的实现,今天周末休息,就把我之前挖的坑给大家填上,本来我打算先给大家分享一个最简单的涉及硬件操作的设备驱动程序的实现——按键/LED驱动的,把这个步进电机的驱动程序留给你们后面先自己做一下,想了想其实都是最基本的,就直接上步进电机吧,下面我们开始学习步进电机驱动程序的实现吧!!
字符设备驱动程序实现的步骤
这个我之前讲过了,再温习一遍
1.确定主设备号,一般设置major = 0,让内核进行自动分配
2.定义一个属于自己的file_operations结构体,这个结构体内定了我们要实现的功能函数
3.实现file_operations结构体内定义的功能函数
4.把file_operations结构体告诉内核:register_chrdev()
5.注册驱动程序,再入口函数中注册,安装设备驱动时,会首先调用这个函数
6.出口函数,卸载驱动程序时,会调用这个函数,在出口函数中卸载驱动,unregister_chrdev()
7.创建设备信息,比如设备节点,设备名称:class_create(), device_create(),便于对设备进行操作
开发板与ULN2003步进电机
这里我用的是stm32mp157开发板,操作系统内核为Linux5.4,给我们预留了一些可供我们使用的GPIO引脚,我这里是GPIOA5、GPIOA13、GPIOD8、GPIOC3,你的开发板肯定和我的不一样,但是只要有四个引脚可用你用就可以了。
ULN步进电机的原理图如下所示,步进电机的驱动原理是依次给IN1->IN2->IN3->IN4输入高电平实现正转,依次给IN4->IN3->IN2->IN1输入高电平实现反转。
现在开发板和步进电机的原理图已经给大家介绍了,依次按照引脚顺序完成硬件连接。我这里的顺序为GPIOA5——IN1,GPIOA13——IN2,GPIOD8——IN3,GPIOC3——IN4。
STM32MP157开发板的GPIO操作函数
首先STM32MP157开发板是双核的,A7和M4,一般A7是用来嵌入式开发的,M4用来做单片机裸机开发,两个核所对应的GPIO的寄存器地址是不一样的,但位定义一样。下面我以其中一个GPIOA5引脚为例来介绍STM32MP157的GPIO的操作,你的开发板怎么操作去参看它的芯片手册即可,原理都是一样的。
1.首先我们需要使能时钟
通过芯片手册可以看到时钟寄存器为RCC_PLL4CR,地址偏移为0x894,在芯片寄存器内存分配章节我找到它的基地址为0x50000000,所以就能得到RCC_PLL4CR的寄存器地址为0x50000000+0x894。从下面可以看出赋值1时使能。
我这里把RCC、GPIOA、GPIOC、GPIOD的基地址都给出来后面我就不介绍了
2.使能所用的引脚
GPIOA5属于GPIOA里的第十个引脚,我们需要使能GPIOA,从下面可以看出RCC_MP_AHB4ENSETR地址:0x50000000 + 0xA28,将此寄存器的第一位使能就使能了GPIOA了。
3.设置GPIOA5的模式
模式设置寄存器为GPIOA_MODER,它的地址为GPIOA_MODER地址:0x50002000 + 0x00。设置GPIOA5为输出模式,即设置bit[11:10] = 0b01。
4.设置GPIOA5的输出电平
上面已经设置了GPIOA5为输出模式了,现在要能给它设置高低电平了,STM32MP157中有一个直接写寄存器。GPIOA_BSRR,地址为0x50002000 + 0x18。
掌握了GPIO的操作之后我们就可以开始写步进电机的驱动程序啦!!
步进电机驱动程序实现
我们按照字符设备驱动程序实现的步骤来一步一步写:
1.主设备号
static int major = 0;//声明主设备号,让内核自动分配
2.定义file_operations结构体
static struct file_operations mtr_drv_fops = {
.owner = THIS_MODULE,
.open = mtr_drv_open,
.write = mtr_drv_write,
.release = mtr_drv_close,
};
3.实现file_operations结构体中的功能函数
static int mtr_drv_open(struct inode *node, struct file *file){
//我们在打开函数中实现对GPIO引脚的初始化操作
//首先设置使能时钟
*RCC_PLL4CR |= (1<<0);
//使能GPIOA、GPIOD、GPIOC
*RCC_MP_AHB4ENSETR |= ((1<<0) + (1<<2) + (1<<3));
//设置为输出模式
//设置GPIOA5、GPIOA13
*GPIOA_MODER &= ~((3<<10) + (3<<26));
*GPIOA_MODER |= ((1<<10) + (1<<26));
//设置GPIOC3
*GPIOC_MODER &= ~(3<<6);
*GPIOC_MODER |= (1<<6);
//设置GPIOD8
*GPIOD_MODER &= ~(3<< 16);
*GPIOD_MODER |= (1<<16);
return 0;
}
static ssize_t mtr_drv_write(struct file *file, const char __user *user, size_t size, loff_t *offset){
//在写函数中实现对GPIO的输出高电平
//从应用程序中得到指令写入到引脚
char kbuf;
copy_from_user(&kbuf, user, 1);
switch(kbuf){
//依次赋高电平
case 1:
*GPIOC_BSRR = (1 << 19);
*GPIOD_BSRR = (1 << 24);
*GPIOA_BSRR = (1 << 29);
*GPIOA_BSRR = (1 << 5);
break;
}
case 2:
*GPIOC_BSRR = (1 << 19);
*GPIOA_BSRR = (1 << 21);
*GPIOD_BSRR = (1 << 24);
*GPIOA_BSRR = (1 << 13);
break;
case 3:
*GPIOA_BSRR = (1 << 21);
*GPIOA_BSRR = (1 << 29);
*GPIOD_BSRR = (1 << 24);
*GPIOC_BSRR = (1 << 3);
break;
case 4:
*GPIOA_BSRR = (1 << 21);
*GPIOA_BSRR = (1 << 29);
*GPIOC_BSRR = (1 << 19);
*GPIOD_BSRR = (1 << 8);
break;
default:
break;
}
return 0;
}
static int mtr_drv_close(struct inode *node, struct file *file){
return 0;
}
4.入口函数,注册file_operations
static int __init mtr_drv_init(void){
printk("%s %s LINE %d\n", __FUNCTIN__, __FILE__, __LINE__);
if(!RCC_PLL4CR){//如果没有映射则映射,已经映射了就不再映射了
//映射ioremap()硬件物理地址
//时钟寄存器地址映射
RCC_PLL4CR = ioremap(0x50000000 + 0x894, 4);
//使能GPIO寄存器地址映射
RCC_MP_AHB4ENSETR = ioremap(0x50000000 + 0xA28, 4);
//设置GPIOA工作方式寄存器地址映射
GPIOA_MODER = ioremap(0x50002000 + 0x00, 0x18);
//设置GPIOC工作方式寄存器地址映射
GPIOC_MODER = ioremap(0x50004000 + 0x00, 4);
//设置GPIOD工作方式寄存器地址映射
GPIOD_MODER = ioremap(0x50005000 + 0x00, 4);
//设置GPIOA写寄存器地址映射
GPIOA_BSRR = ioremap(0x50002000 + 0x18, 0x18);
//设置GPIOC写寄存器地址映射
GPIOC_BSRR = ioremap(0x50004000 + 0x18, 4);
//设置GPIOD写寄存器地址映射
GPIOD_BSRR = ioremap(0x50005000 + 0x18, 4);
}
//注册file_operations结构体
major = register_chrdev(0, "mtr_drv", &mtr_drv_fops);//设备名
mtr_drv_class = class_create(THIS_MODULE, "mtr_drv");
device_create(mtr_drv_class, NULL, MKDEV(major, 0), NULL, "mtr_drv0");//设备节点
return 0;
}
5.出口函数
static void __exit mtr_drv_exit(void){
iounmap(RCC_PLL4CR);
iounmap(RCC_MP_AHB4ENSETR);
iounmap(GPIOA_MODER);
iounmap(GPIOC_MODER);
iounmap(GPIOD_MODER);
iounmap(GPIOA_BSRR);
iounmap(GPIOC_BSRR);
iounmap(GPIOD_BSRR);
device_destroy(mtr_drv_class, MKDEV(major, 0));
class_destroy(mtr_drv_class);
unregister_chrdev(major, "mtr_drv");
return;
}
6.其他补充
//告诉内核谁是入口、出口函数
module_init(mtr_drv_init);
module_exit(mtr_drv_exit);
MODULE_LICENSE("GPL");
还有一些声明:
static struct class* mtr_drv_class;
//寄存器声明
static volatile unsigned int *RCC_PLL4CR;
static volatile unsigned int *RCC_MP_AHB4ENSETR;
static volatile unsigned int *GPIOA_MODER;
static volatile unsigned int *GPIOC_MODER;
static volatile unsigned int *GPIOD_MODER;
static volatile unsigned int *GPIOA_BSRR;
static volatile unsigned int *GPIOC_BSRR;
static volatile unsigned int *GPIOD_BSRR;
到这里整个步进电机的驱动程序就完成了。
在Ubuntu中用交叉编译工具编译成hello_drv.ko文件,就可以在开发板上运行.
insmod mtr_drv.ko:加载安装驱动程序
ls /dev/:查看设备节点,看是否有我们安装的hello设备节点
cat /proc/devices:查看所有的设备文件,包括主设备号和设备名
lsmod:查看已经加载的设备驱动
rmmod mtr_drv:卸载驱动
测试程序和完整驱动程序的源码我都放在公众号中了,需要的关注公众号回复“步进电机驱动程序”即可获取。有什么问题可以在下方留言,我看到之后会回复你。
我是河边小乌龟爬,学习嵌入式软件开发路上的一名小学生,欢迎大家相互交流哇。公众号:河边小乌龟爬。QQ群名称:嵌入式软件开发群:1004953094。