米联客MLK-L2-CZ06-7020 ZYNQ7020 Linux驱动HelloWorld实战文档
文档基础信息
文档名称:MLK-L2 ZYNQ7020 Linux字符驱动HelloWorld实战教程
适用硬件:米联客MLK-L2-CZ06-7020(XC7Z020)
配套教程:米联客2024版ZYNQ Linux驱动开发篇 第1章
软件环境:Vivado2021.1 + Vitis2021.1 + 米联客Ubuntu交叉编译虚拟机
适配人群:ZYNQ嵌入式Linux驱动零基础学习者
一、实验环境说明
1.1 硬件平台
米联客MLK-L2-CZ06-7020 ZYNQ7020开发板,搭载双核Cortex-A9处理器,支持SD卡启动、串口调试、网口SSH文件传输,是嵌入式ZYNQ Linux驱动开发的入门主流硬件平台。
1.2 全套软件环境
1. Windows端开发工具:Vivado 2021.1、Vitis 2021.1
2. 交叉编译虚拟机:米联客官方uisrc-lab-xlnx Ubuntu编译环境
3. 交叉编译工具链:arm-linux-gnueabihf
4. 开发板系统:米联客预编译Ubuntu根文件系统镜像
1.3 实验学习目标
1. 区分Linux三大设备:字符设备、块设备、网络设备,熟练掌握字符设备应用场景;
2. 吃透标准Linux字符驱动完整模块化开发模板,掌握驱动基础框架;
3. 熟练使用class_create、device_create函数自动创建设备节点;
4. 掌握copy_to_user、copy_from_user函数,实现内核与用户空间数据交互;
5. 打通完整开发链路:Vivado硬件工程搭建 → Vitis设备树配置 → 虚拟机交叉编译 → 开发板模块加载与调试。
1.4 基础核心原理
Linux操作系统严格划分为用户空间(应用程序APP)和内核空间(驱动程序),两个空间内存相互隔离,无法直接通过指针访问数据,必须依靠专用拷贝函数完成数据传递。
字符设备以字节流顺序读写数据,无随机访问特性,日常开发中LED、按键、串口、ADC外设均属于典型字符设备。
Linux设备号分为主设备号与次设备号,主设备号用于区分不同类型的驱动程序,次设备号用于区分同一驱动下的多个外设。本次实验采用动态分配主设备号方式,系统可自动在/dev目录生成设备文件/dev/KernelPrint_0。
二、Vivado+Vitis硬件工程搭建流程
2.1 Vivado导出XSA硬件描述文件
1. 打开米联客配套soc_prj工程,双击system.bd块设计文件;
2. 双击ZYNQ7020 IP核,采用米联客默认PS配置,开启DDR、SD、UART、GPIO全部基础外设;
3. 点击菜单栏Generate Bitstream,编译生成PL比特流文件;
4. 依次点击菜单File → Export → Export Hardware,弹窗勾选Include bitstream,导出至soc_hw文件夹,最终生成system_wrapper.xsa硬件描述文件。
2.2 Vitis创建设备树工程
1. 打开Vitis软件,新建工作空间并命名为soc_sdk;
2. 点击Xilinx → Software Repositories,导入配套device-tree-xlnx设备树模板;
3. 新建平台工程File → New → Platform Project,工程名设置为soc_base,导入上一步生成的xsa文件;
4. 操作系统选择device_tree,处理器默认Cortex-A9;
5. 点击编译按钮,自动生成fsbl.elf、system_wrapper.bit、基础设备树文件,备用。
2.3 虚拟机编译SD系统镜像
1. 将编译完成的bit、fsbl、dts设备树文件拷贝至Ubuntu虚拟机;
2. 终端进入米联客环境目录:/home/uisrc/uisrc-lab-xlnx;
3. 依次执行环境编译脚本:
bash source scripts/mz7xcfg.sh ./move_files.sh ./make_uboot.sh ./make_kernel.sh ./create_image.sh |
4. 将生成的完整SD系统镜像烧录至SD卡,插入MLK-L2开发板,拨码开关调整为SD启动模式(ON OFF OFF)。
三、全套可直接运行源码
3.1 内核驱动程序 KernelPrint.c
c #include <linux/init.h> #include <linux/module.h> #include <linux/uaccess.h>
static char readbuf[100]; static char writebuf[100]; static char message[] = "This message comes from kernel."; static int drive_major; static struct class *KernelPrint_cls;
// 设备打开函数 static int KernelPrint_open(struct inode *inode, struct file *filp) { printk("-KernelPrint open-\n"); return 0; }
// 内核向应用层发送数据 static ssize_t KernelPrint_read(struct file *filp, char __user *buf, size_t count, loff_t *fops) { int flag = 0; memcpy(readbuf, message, sizeof(message)); flag = copy_to_user(buf, readbuf, count); if(flag == 0) printk("Kernel send data success!\n"); else printk("Kernel send data failed!\n"); printk("-KernelPrint read-\n"); return 0; }
// 应用层向内核写入数据 static ssize_t KernelPrint_write(struct file *filp, const char __user *buf, size_t count, loff_t *fops) { int flag = 0; flag = copy_from_user(writebuf, buf, count); if(flag == 0) printk(KERN_CRIT "Kernel receive data: %s\n", writebuf); else printk("Kernel receive data failed!\n"); printk("-KernelPrint write-\n"); return 0; }
// 设备关闭释放函数 static int KernelPrint_release(struct inode *inode, struct file *filp) { printk("-KernelPrint release-\n"); return 0; }
// 文件操作绑定结构体 static struct file_operations drive_fops = { .owner = THIS_MODULE, .open = KernelPrint_open, .read = KernelPrint_read, .write = KernelPrint_write, .release = KernelPrint_release, };
// 驱动加载入口函数 static __init int KernelPrint_init(void) { printk("-------^v^-------\n"); printk("-KernelPrint init-\n"); // 动态申请主设备号 drive_major = register_chrdev(0, "KernelPrint", &drive_fops); if(drive_major < 0){ printk("register chrdev faile!\n"); return drive_major; } printk("register chrdev ok!\n");
// 创建设备类 KernelPrint_cls = class_create(THIS_MODULE, "KernelPrint_class"); printk("class create ok!\n"); // 自动生成/dev下设备节点 device_create(KernelPrint_cls, NULL, MKDEV(drive_major, 0), NULL, "KernelPrint%d", 0); printk("device create ok!\n"); return 0; }
// 驱动卸载入口函数 static __exit void KernelPrint_exit(void) { printk("-------^v^-------\n"); printk("-KernelPrint exit-\n"); // 反向释放所有资源 device_destroy(KernelPrint_cls, MKDEV(drive_major, 0)); class_destroy(KernelPrint_cls); unregister_chrdev(drive_major, "KernelPrint"); }
module_init(KernelPrint_init); module_exit(KernelPrint_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("msxbo"); |
3.2 用户层测试程序 KernelPrintApp.c
c #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <string.h>
int main(int argc, char *argv[]) { int fd, retvalue; char *filename; char readbuf[100], writebuf[100]; filename = argv[1];
fd = open(filename, O_RDWR); if(fd < 0){ printf("Can't open file %s\n", filename); return -1; }
switch (*argv[2]) { case 'r': if(argc != 3){ printf("读取用法:./KernelPrintApp /dev/KernelPrint_0 r\n"); return -1; } retvalue = read(fd, readbuf, 100); if(retvalue < 0) printf("Read failed!\n"); else printf("User receive data: %s\n", readbuf); break; case 'w': if(argc != 4){ printf("写入用法:./KernelPrintApp /dev/KernelPrint_0 w 自定义字符串\n"); return -2; } memcpy(writebuf, argv[3], strlen(argv[3])); retvalue = write(fd, writebuf, 50); if(retvalue < 0) printf("Write file failed!\n"); else printf("Write file success!\n"); break; default: printf("未知操作!仅支持 r(读) / w(写)\n"); break; } close(fd); return 0; } |
3.3 交叉编译Makefile文件
makefile # 米联客固定内核路径,请勿修改 KERNEL_DIR = /home/uisrc/uisrc-lab-xlnx/sources/kernel export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- CURRENT_DIR = $(shell pwd) MODULE = KernelPrint APP = KernelPrintApp
all : make -C $(KERNEL_DIR) M=$(CURRENT_DIR) modules rm -rf *.symvers *.order *.o *.mod.o *.mod.c ifneq ($(APP), ) $(CROSS_COMPILE)gcc $(APP).c -o $(APP) endif
clean : make -C $(KERNEL_DIR) M=$(CURRENT_DIR) clean rm $(APP) obj-m += $(MODULE).o |
四、虚拟机交叉编译操作步骤
1. 将KernelPrint.c、KernelPrintApp.c、Makefile三个文件放置在同一文件夹,上传至米联客Ubuntu虚拟机;
2. 在文件目录下打开终端,执行make命令进行编译;
3. 编译产物说明:KernelPrint.ko为Linux内核驱动模块,KernelPrintApp为ARM架构可执行测试程序;
4. 通过Xshell、PSCP、U盘等方式,将两个编译产物传输至MLK-L2开发板。
五、开发板完整测试流程
5.1 硬件连接要求
MLK-L2-CZ06-7020开发板插入烧录好系统的SD卡,拨码开关设置为SD启动模式;USB串口连接电脑用于调试打印,可接网线实现SSH高速文件传输。
5.2 开发板终端执行命令
1. 文件权限配置
bash ls KernelPrint.ko KernelPrintApp chmod 777 KernelPrintApp |
2. 加载驱动模块
bash sudo insmod KernelPrint.ko lsmod ls /dev/KernelPrint_0 |
3. 读取测试(应用读取内核预设字符串)
bash ./KernelPrintApp /dev/KernelPrint_0 r |
预期输出:User receive data: This message comes from kernel.
4. 写入测试(应用向内核传输自定义字符串)
bash ./KernelPrintApp /dev/KernelPrint_0 w Hello MLK-L2 ZYNQ7020 dmesg | tail |
内核预期打印:Kernel receive data: Hello MLK-L2 ZYNQ7020
5. 卸载驱动模块
bash sudo rmmod KernelPrint |
六、核心知识点总结
1. 字符驱动标准接口:open、read、write、release,依靠file_operations结构体完成应用程序与驱动的绑定;
2. 空间数据交互规则:copy_to_user实现内核数据传输至用户空间,copy_from_user实现用户数据传输至内核空间;
3. 自动创建设备流程:register_chrdev注册驱动 → class_create创建设备类 → device_create生成/dev设备节点,卸载流程反向执行;
4. 内核模块规范:module_init为模块加载入口、module_exit为模块卸载入口,必须配置MODULE_LICENSE("GPL"),避免内核污染警告;
5. 硬件适配要点:MLK-L2开发板必须使用米联客配套编译环境,禁止随意修改内核路径、交叉工具链配置,避免编译报错。
七、常见问题与解决方案
1. make编译报错找不到内核
原因:虚拟机未配置环境变量;解决方案:进入米联客环境目录,执行source scripts/mz7xcfg.sh后重新编译。
2. insmod加载驱动提示版本不匹配
原因:编译内核源码与开发板运行内核版本不一致;解决方案:使用米联客原厂配套内核源码编译驱动。
3. 无法打开/dev/KernelPrint_0设备
原因:驱动未加载或文件权限不足;解决方案:重新加载驱动,使用root权限执行测试程序。
4. 读写数据为空、乱码
原因:缓冲区长度溢出、数据拷贝参数错误;解决方案:核对缓冲区大小,保证拷贝参数不越界。
5. 串口无调试打印信息
原因:开发板拨码错误、串口波特率不匹配;解决方案:确认SD启动拨码,串口工具波特率设置为115200。
八、配套资源信息
官方技术网站:https://www.uisrc.com
配套教程:米联客2024版ZYNQ Linux驱动开发篇 第1章
开发板型号:MLK-L2-CZ06-7020 ZYNQ7020