已经入门单片机的初学者都知道,led驱动是最简单的驱动之一,单片机入门资料经常以流水灯或跑马灯的例子来展示单片机的使用方法。下面以飞思卡尔16位单片机MC9S12XS128为例,让我们来看一下单片机是怎么实现对led驱动的。这款16位单片机多用于学生的入门学习和电子竞赛中,它的编程软件Codewarrior在创建工程的时候已经对内存映射做了定义,用户可以直接使用PORTA和PORTA_PA0这样的宏进行IO口的操作,于是我们的LED驱动可以这么写:
#define LED PORTA_PA0
#define LED_DIR DDRA_DDRA0
#define INPUT 0
#define OUTPUT 1
#define ON 1
#define OFF 0
void light_init(void)
{
LED_DIR = OUTPUT;
LED = OFF;
}
void light_on(void)
{
LED = ON;
}
void light_off(void)
{
LED = OFF;
}
我们来看看驱动做了什么事:1、初始化的时候将LED相关的GPIO方向寄存器设置为输出,并将数据寄存器写0,使LED灯在初始化后熄灭;2、提供light_on和light_off函数,用户(自己或其他程序员)在程序中直接调用这两个函数就能实现LED灯的亮和灭。为了能深入理解寄存器的操作,下面我们不使用Codewarrior提供的寄存器宏,直接对地址进行操作。首先在芯片的datasheet中找到地址映射,在MC9S12系列芯片datasheet的末尾可以看到下面的映射:
我们将上面的代码做简单的修改,使用直接操作地址的方式:
#define PORTA_BASE 0x0000
#define DDRA_BASE 0x0002
typedef struct{
volatile unsigned int bit0 : 1;
volatile unsigned int bit1 : 1;
volatile unsigned int bit2 : 1;
volatile unsigned int bit3 : 1;
volatile unsigned int bit4 : 1;
volatile unsigned int bit5 : 1;
volatile unsigned int bit6 : 1;
volatile unsigned int bit7 : 1;
}PORT;
#define PORT_A (PORT *)PORTA_BASE
#define DDR_A (PORT *)DDRA_BASE
PORT *port_a = PORT_A, *ddra = DDR_A;
#define LED port_a->bit0
#define LED_DIR ddra->bit0
上面的代码只是添加了对地址的映射,对应的驱动函数可以不对应修改。有了上面的单片机驱动led基础,我们来看看linux下是怎么对Led进行操作的,使用的硬件是友善之臂出品的NanoPi NEO Plus2开发板。基本的IO寄存器操作都是一样的,都是对IO口方向和电平的设置,根据datasheet中的内容,对IO寄存器进行如下操作:
#define PIO_BASE 0x01f02c00
#define PL_CFG1 (PIO_BASE + 0x04)
#define PL_DATA (PIO_BASE + 0x10)
enum IO_STATE{
INPUT,
OUTPUT,
S_PWM,
S_PL_EINT = 6,
IO_DISABLE,
};
enum LIGHT_STATE{LIGHT_OFF,LIGHT_ON};
typedef struct {
volatile unsigned int PL8_SELECT : 3;
volatile unsigned int res0 : 1;
volatile unsigned int PL9_SELECT : 3;
volatile unsigned int res1 : 1;
volatile unsigned int PL10_SELECT : 3;
volatile unsigned int res2 : 1;
volatile unsigned int PL11_SELECT : 3;
volatile unsigned int res3 : 17;
}pl_cfg1;
typedef struct {
volatile unsigned int GPIO_0 : 1;
volatile unsigned int GPIO_1 : 1;
volatile unsigned int GPIO_2 : 1;
volatile unsigned int GPIO_3 : 1;
volatile unsigned int GPIO_4 : 1;
volatile unsigned int GPIO_5 : 1;
volatile unsigned int GPIO_6 : 1;
volatile unsigned int GPIO_7 : 1;
volatile unsigned int GPIO_8 : 1;
volatile unsigned int GPIO_9 : 1;
volatile unsigned int GPIO_10 : 1;
volatile unsigned int GPIO_11 : 1;
volatile unsigned int res : 20;
}pl_data;
pl_cfg1 *GPIOL_GREEN_CFG;
pl_data *GPIOL_GREEN_DATA;
void light_init(void)
{
GPIOL_GREEN_CFG = (pl_cfg1 *)ioremap(PL_CFG1,4);
GPIOL_GREEN_DATA = (pl_data *)ioremap(PL_DATA,4);
GPIOL_GREEN_CFG->PL10_SELECT = OUTPUT;
GPIOL_GREEN_DATA->GPIO_10 = LIGHT_OFF;
}
void light_on(void)
{
printk("%s
",__func__);
GPIOL_GREEN_DATA->GPIO_10 = LIGHT_ON;
}
void light_off(void)
{
printk("%s
",__func__);
GPIOL_GREEN_DATA->GPIO_10 = LIGHT_OFF;
}
从上面的代码可以看出,基本的操作流程都是一样的,但是在Linux操作系统中cpu只能对虚拟地址进行操作,不能直接操作物理地址,因此在light_init函数中使用了ioremap将物理地址翻译成虚拟地址。虚拟地址、物理地址和MMU的概念不在本次的介绍范围,还请读者自己查资料学习。有了操作函数后怎么使用呢?首先来看一下linux软件层次的划分,linux将存储空间划分为内核空间和用户空间,内核相关的代码能对进行内核级别的访问,同时也能对硬件进行操作,而用户空间属于上层软件的操作,如果想要从用户控件访问内核空间的一些内容,就需要使用Linux操作系统提供的一些驱动接口。linux驱动包含字符设备、块设备和网络设备驱动,字符设备是最基础的驱动,下面是从用户控件通过字符设备访问内核空间的框架。
结合图中的内容,字符设备提供了一些基本的read(),write(),ioctl()等操作函数,将其封装在file_operation结构体中,用来实现内核空间的访问,当注册了字符设备驱动后,在用户空间就可以使用对应的文件操作函数read(),write(),ioctl()通过系统调用,实现用户空间对内核空间的访问。file_operations结构提供的函数如下:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
u64);
};
在使用时,用户只需要定义对应的结构体,并填充其中需要使用的函数即可,本例中的函数和结构体定义如下:
ssize_t char_test_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
printk("%s
",__func__);
printk("GPIOL_GREEN_CFG = 0x%x
",*(unsigned int *)GPIOL_GREEN_CFG);
printk("GPIOL_GREEN_DATA = 0x%x
",*(unsigned int *)GPIOL_GREEN_DATA);
return 0;
}
ssize_t char_test_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
char argv;
printk("%s
",__func__);
if(copy_from_user(&argv,buf,count))
printk(KERN_INFO"Copy from user data ERROR!
");
if(argv == '1')
light_on();
else light_off();
return count;
}
long char_test_ioctl(struct file *filp, unsigned int cmd,
unsigned long count)
{
return count;
}
int char_test_open(struct inode *inode, struct file *filp)
{
printk("%s
",__func__);
return 0;
}
int char_test_release(struct inode *inode, struct file *filp)
{
printk("%s
",__func__);
return 0;
}
struct file_operations char_test_ops = {
.owner = THIS_MODULE,
.read = char_test_read,
.write = char_test_write,
.unlocked_ioctl = char_test_ioctl,
.open = char_test_open,
.release = char_test_release,
};
有了file_operations结构后,只要再实现字符设备的注册和注销函数即可:
static int __init char_test_init(void)
{
int ret;
printk("%s
",__func__);
light_init();
ret = register_chrdev(CHAR_DEV_MAJOR,CHAR_DEV_NAME,&char_test_ops);
if(ret){
printk("Can't register char device %d
",CHAR_DEV_MAJOR);
return ret;
}
return 0;
}
module_init(char_test_init);
static void __exit char_test_exit(void)
{
unregister_chrdev(CHAR_DEV_MAJOR,CHAR_DEV_NAME);
printk("%s
",__func__);
}
module_exit(char_test_exit);
一切准备就绪,接下来将其编译成内核模块,然后使用insmod进行注册即可,编译和注册的步骤可以在这里查看:
点击打开链接模块加载后,通过cat /proc/devices来查看是否正确加载,确认加载成功后即可使用mknod /dev/led_test c 234 0 命令在/dev目录下创建led_test的设备节点,命令中的234和0代表主设备号和次设备号,c代表字符设备。进入/dev/目录,使用echo 1 > led_test和echo 0 > led_test命令可以实现led的亮灭,这个操作调用的是驱动中的char_test_write函数,也可以创建一个main.c文件,写入如下内容:
#include
#include
#include
#include
#include
#include
int main(int argc, char **argv)
{
int fd,len;
int value = 0;
fd = open("/dev/my_char_dev",O_RDWR);
if(fd){
printf("open device ok!
");
write(fd,"a",1);
close(fd);
}
else {
printf("can't open "/dev/my_char_dev"!
");
return 1;
}
fd = open("/dev/my_char_dev",O_RDWR);
len = read(fd,&value,1);
ioctl(fd,1);
printf("value = %d
",value);
close(fd);
return 0;
}
使用gcc main.c -o test命令将其编译成可执行文件,使用./test即可实现led的操作。