【原创】Linux下驱动Zynq GPIO (Switch、button、led)
2赞版权声明:
本文由电子技术应用博主“cuter”发布。欢迎转载,但不得擅自更改博文内容,也不得用于任何盈利目的。转载时不得删除作者简介和作者单位简介。如有盗用而不说明出处引起的版权纠纷,由盗用者自负。
博客官方地址:
http://blog.chinaaet.com/cuter521
http://bbs.ednchina.com/BLOG_cuter521_356737.HTM
1 硬件设计:
1.1) Vivado Block Design
1.1.1) 设计自主AXI-Lite IP核
关键逻辑如下:
下图所示的是寄存器读操作,主要目的是为了读取led、switch和button的状态。这里并没有做任何处理,对于button而言,可以利用硬件进行预处理之后,将结果传递给软件,这样可以减少软件工作量。
下图所示的逻辑用于将寄存器内的值送至led引脚。
IP制作方法,参考以前的博文《Vivado下创建基于AXI-Lite的用户IP核》
1.1.2) 添加IP核至block design
完成后的block design如下图所示。
1.1.3) 编写约束文件
led、switch和button所用的引脚都需要约束。使用Run Automation的时候,Vivado会帮助完成约束,我们自主IP暂时未实现Run Automation功能,所以要手动约束。约束代码如下:
#NET LD0 LOC = T22 | IOSTANDARD=LVCMOS33; # "LD0"
set_property PACKAGE_PIN T22 [get_ports {zed_led[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {zed_led[0]}]
#NET LD1 LOC = T21 | IOSTANDARD=LVCMOS33; # "LD1"
set_property PACKAGE_PIN T21 [get_ports {zed_led[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {zed_led[1]}]
#NET LD2 LOC = U22 | IOSTANDARD=LVCMOS33; # "LD2"
set_property PACKAGE_PIN U22 [get_ports {zed_led[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {zed_led[2]}]
#NET LD3 LOC = U21 | IOSTANDARD=LVCMOS33; # "LD3"
set_property PACKAGE_PIN U21 [get_ports {zed_led[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {zed_led[3]}]
#NET LD4 LOC = V22 | IOSTANDARD=LVCMOS33; # "LD4"
set_property PACKAGE_PIN V22 [get_ports {zed_led[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {zed_led[4]}]
#NET LD5 LOC = W22 | IOSTANDARD=LVCMOS33; # "LD5"
set_property IOSTANDARD LVCMOS33 [get_ports {zed_led[5]}]
set_property PACKAGE_PIN W22 [get_ports {zed_led[5]}]
#NET LD6 LOC = U19 | IOSTANDARD=LVCMOS33; # "LD6"
set_property PACKAGE_PIN U19 [get_ports {zed_led[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {zed_led[6]}]
#NET LD7 LOC = U14 | IOSTANDARD=LVCMOS33; # "LD7"
set_property IOSTANDARD LVCMOS33 [get_ports {zed_led[7]}]
set_property PACKAGE_PIN U14 [get_ports {zed_led[7]}]
#NET SW0 LOC = F22 | IOSTANDARD=LVCMOS18; # "SW0"
set_property PACKAGE_PIN F22 [get_ports {zed_sw[0]}]
set_property IOSTANDARD LVCMOS18 [get_ports {zed_sw[0]}]
#NET SW1 LOC = G22 | IOSTANDARD=LVCMOS18; # "SW1"
set_property PACKAGE_PIN G22 [get_ports {zed_sw[1]}]
set_property IOSTANDARD LVCMOS18 [get_ports {zed_sw[1]}]
#NET SW2 LOC = H22 | IOSTANDARD=LVCMOS18; # "SW2"
set_property PACKAGE_PIN H22 [get_ports {zed_sw[2]}]
set_property IOSTANDARD LVCMOS18 [get_ports {zed_sw[2]}]
#NET SW3 LOC = F21 | IOSTANDARD=LVCMOS18; # "SW3"
set_property PACKAGE_PIN F21 [get_ports {zed_sw[3]}]
set_property IOSTANDARD LVCMOS18 [get_ports {zed_sw[3]}]
#NET SW4 LOC = H19 | IOSTANDARD=LVCMOS18; # "SW4"
set_property PACKAGE_PIN H19 [get_ports {zed_sw[4]}]
set_property IOSTANDARD LVCMOS18 [get_ports {zed_sw[4]}]
#NET SW5 LOC = H18 | IOSTANDARD=LVCMOS18; # "SW5"
set_property PACKAGE_PIN H18 [get_ports {zed_sw[5]}]
set_property IOSTANDARD LVCMOS18 [get_ports {zed_sw[5]}]
#NET SW6 LOC = H17 | IOSTANDARD=LVCMOS18; # "SW6"
set_property PACKAGE_PIN H17 [get_ports {zed_sw[6]}]
set_property IOSTANDARD LVCMOS18 [get_ports {zed_sw[6]}]
#NET SW7 LOC = M15 | IOSTANDARD=LVCMOS18; # "SW7"
set_property PACKAGE_PIN M15 [get_ports {zed_sw[7]}]
set_property IOSTANDARD LVCMOS18 [get_ports {zed_sw[7]}]
#NET BTNC LOC = P16 | IOSTANDARD=LVCMOS18; # "BTNC"
set_property IOSTANDARD LVCMOS18 [get_ports {zed_btn[0]}]
set_property PACKAGE_PIN P16 [get_ports {zed_btn[0]}]
# BTNU
set_property IOSTANDARD LVCMOS18 [get_ports {zed_btn[1]}]
set_property PACKAGE_PIN T18 [get_ports {zed_btn[1]}]
# BTND
set_property IOSTANDARD LVCMOS18 [get_ports {zed_btn[2]}]
set_property PACKAGE_PIN R16 [get_ports {zed_btn[2]}]
# BTNL
set_property IOSTANDARD LVCMOS18 [get_ports {zed_btn[3]}]
set_property PACKAGE_PIN N15 [get_ports {zed_btn[3]}]
# BTNR
set_property IOSTANDARD LVCMOS18 [get_ports {zed_btn[4]}]
set_property PACKAGE_PIN R18 [get_ports {zed_btn[4]}]
1.1.4) 生成bitstream
1.2) 制作BOOT.bin
1.3) 修改dts文件
小改动,为简单起见,保持和驱动程序中的设备名称一致。IP的物理地址一定要改!
2 驱动设计:
2.1) digilent驱动学习
首先,姑且不管每个函数的具体作用,我们将驱动程序的框架剥离出来进行分析,这样程序结构更加清晰。
2.1.1) platform_driver成员函数
//设备的驱动:platform_driver这个结构体中包含probe()、remove()、shutdown()、suspend()、 resume()函数,通常也需要由驱动实现。
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*suspend_late)(struct platform_device *, pm_message_t state);
int (*resume_early)(struct platform_device *);
int (*resume)(struct platform_device *);
struct pm_ext_ops *pm;
struct device_driver driver;
};
驱动程序对platform_driver进行初始化的代码:
/* platform driver structure for mygpio driver */
static struct platform_driver mygpio_driver =
{
.driver =
{
.name = DRIVER_NAME,
.owner = THIS_MODULE,
.of_match_table = mygpio_of_match
},
.probe = mygpio_probe,
.remove = __devexit_p(mygpio_remove),
.shutdown = __devexit_p(mygpio_shutdown)
};
这些函数的命名本身具有一定的自明性,此段代码进一步阐明了probe()、remove()和shutdown()函数的作用,具体每个函数的作用可以参考函数体前面的注释,写得很详细。
2.1.2) 文件操作函数
关键代码:
static const struct file_operations proc_mygpio_operations = {
.open = proc_mygpio_open,
.read = seq_read,
.write = proc_mygpio_write,
.llseek = seq_lseek,
.release = single_release
};
文件操作深究起来,也能独立成文了,对于字符设备而言,要提供的主要入口有:open()、release()、read()、write()、ioctl()、llseek()、poll()等,这里简单说一下用到的几个函数。
loff_t (*llseek) (struct file *, loff_t, int);
llseek 方法用作改变文件中的当前读/写位置,并且新位置作为(正的)返回值。
ssize_t (*read) (struct file * filp, char __user * buffer, size_t size , loff_t * p);
这个函数用来从设备中获取数据。
ssize_t (*write) (struct file * filp, const char __user * buffer, size_t count, loff_t * ppos);
发送数据给设备。
int (*open) (struct inode * inode , struct file * filp ) ;
对设备文件进行open()系统调用时,将调用驱动程序的open()函数。该函数主要作用是确定硬件处在就绪状态、验证次设备号的合法性、控制使用设备的进程数、根据执行情况返回状态量等。
int (*release) (struct inode *, struct file *);
release()函数在文件结构被释放时引用这个操作,当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数。release()函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。
2.2) 驱动修改
名字什么的就不多说了,必然要改的,最大的改动在于proc_xxxx_show(),添加了寄存器读取操作,具体代码如下:
static int proc_mygpio_show(struct seq_file *p, void *v)
{
u32 mygpio_value;
mygpio_value = ioread32(base_addr); // read out data
seq_printf(p, "led = 0x%x ", mygpio_value);
mygpio_value = ioread32(base_addr+0x01); // read out data
seq_printf(p, "switch = 0x%x ", mygpio_value);
mygpio_value = ioread32(base_addr+0x02); // read out data
seq_printf(p, "button = 0x%x ", mygpio_value);
mygpio_value = ioread32(base_addr+0x03); // read out data
seq_printf(p, "reg3 = 0x%x ", mygpio_value);
return 0;
}
Ps~关于这块,我在想,既然修改的地方这么具有规律性,那么是不是能够设计出一种方法自动创建驱动程序模板,从而可以把精力集中在读写函数的实现上来?
3 测试结果:
开关这块稍微有点问题:bit3状态读取结果始终为1,改变sw位置无效,具体原因待查。Button和led正常(测试button时,长按5个按键中间一个BTNC)。
附1: 错误笔记
正所谓无知者无畏,在没有完全掌握一些知识就妄下定论,是不负责任的,以后要多注意。
上一篇博文提到:“这里发现一点小问题,初始化proc_myled_opertaions.read时使用了seq_read,但在驱动程序里定义的读函数却是proc_myled_show,在实际使用时,读led状态也是失败的。所以这里的初始化应该是有问题的,下次要改掉测试一下。”最近又深入学习了驱动程序里的各个函数,发现使用seq_read对proc_myled_operations.read进行初始化是正确的,是一种“套路”。
调用cat指令时,系统首先会调用proc_myled_open()函数,在open()函数内会调用proc_myled_show将读取到的数据存入seq_file。读失败的真正原因是Vivado中使用的axi-gpio IP核导致,该IP核的引脚用作输入时,需要操作方向寄存器,将引脚设为输入才可以读取到引脚状态。但我尝试操作方向寄存器总是失败,换成自己的IP,操作多个寄存器又没有问题,不知道咋回事……