1 前言
笔者使用的是韦东山STM32MP157 Pro的板子,环境搭建部分按照说明文档配置完成。配置桥接网卡实现板子、windows、ubuntu的通信,也在开发板挂载 Ubuntu 的NFS目录 ,这里就不再赘述了。
板子: 192.168.5.9
windows: 192.168.5.10
ubuntu: 192.168.5.11
在板子上执行
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs/ /mnt
2 开发板的第 1 APP 个实验
hello.c
/*************************************************************************> File Name: hello.c> Author: Winter> Created Time: Sat 06 Jul 2024 04:44:00 AM EDT************************************************************************/#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>int main(int argc, char* argv[])
{if (argc >= 2)printf("Hello, %s!\n", argv[1]);elseprintf("Hello, world!\n");return 0;
}
在ubuntu上编译运行
这个程序不是能直接在开发板上运行的,需要使用arm版的工具链
在ubuntu上使用开发板的工具链重新编译,就可以在开发板上执行了
arm-buildroot-linux-gnueabihf-gcc hello.c
3 开发板的第 1 驱动实验
为什么编译驱动程序之前要先编译内核?
驱动程序要用到内核文件:内核/设备树/其他驱动程序
比如驱动程序中这样包含头文件: #include <asm/io.h>,其中的 asm 是一个链接文件,指向 asm-arm 或 asm-mips,这需要先配置、编译内核才会生成 asm 这个链接文件。
编译驱动时用的内核、开发板上运行到内核,要一致:放到板子上
开发板上运行到内核是出厂时烧录的,你编译驱动时用的内核是你自己编译的,这两个内核不一致时会导致一些问题。所以我们编译驱动程序前,要把自己编译出来到内核放到板子上去,替代原来的内核。
更换板子上的内核后,板子上的其他驱动也要更换:编译测试第一个驱动程序
板子使用新编译出来的内核时,板子上原来的其他驱动也要更换为新编译出来的。所以在编译我们自己的第 1 个驱动程序之前,要先编译内核、模块,并且放到板子上去
3.1 编译内核
不同的开发板对应不同的配置文件, 配置文件位于内核源码arch/arm/configs/目录。 kernel 的编译过程如下:
cd 100ask_stm32mp157_pro-sdk/Linux-5.4/
make 100ask_stm32mp157_pro_defconfig
编译内核
make uImage LOADADDR=0xC2000040 -j10
等待,结果如下
编译设备树
make dtbs
编译完成后, 在 arch/arm/boot 目录下生成 uImage 内核文件, 在arch/arm/boot/dts 目录下生成设备树的二进制文件 stm32mp157c-100ask-512d-v1.dtb。把这 2 个文件复制到/home/book/nfs_rootfs 目录下备用
cp arch/arm/boot/uImage ~/nfs_rootfs/
cp arch/arm/boot/dts/stm32mp157c-100ask-512d-v1.dtb ~/nfs_rootfs/
3.2 编译安装内核模块
进入内核源码目录后,就可以编译内核模块了:
cd /home/book/100ask_stm32mp157_pro-sdk/Linux-5.4
make ARCH=arm CROSS_COMPILE=arm-buildroot-linux-gnueabihf- modules -j10
内核模块编译完成后如图
安装内核模块到 Ubuntu 某个目录下备用
可以先把内核模块安装到 nfs 目录(/home/book/nfs_rootfs)。注意: 后面会使用 tree 命令查看目录结构, 如果提示没有该命令, 需要执行以下命令安装 tree 命令:
sudo apt install tree
把模块安装在 nfs 目录“ /home/book/nfs_rootfs/” 下
make ARCH=arm INSTALL_MOD_PATH=/home/book/nfs_rootfs INSTALL_MOD_STRIP=1 modules_install
安装好驱动后的/home/book/nfs_rootfs/目录结构如图
tree /home/book/nfs_rootfs/
3.3 安装内核和模块到开发板上
假设:在 Ubuntu 的/home/book/nfs_rootfs 目录下, 已经有了 zImage、dtb 文件,并且有 lib/modules 子目录(里面含有各种模块)。 接下来要把这些文件复制到开发板上。假设 Ubuntu IP 为 192.168.5.11,在开发板上执行以下命令:
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
mount /dev/mmcblk2p2 /boot
cp /mnt/uImage /boot # 内核
cp /mnt/*.dtb /boot # 设备树
cp /mnt/lib/modules /lib -rfd # 模块
sync
reboot
后面#是注释,不用粘上去
最后重启开发板,它就使用新的 zImage、 dtb、模块了
这里有个问题,图中标出来的地方,问题不大,参考:vmmcsd_fixed: disabling 自动弹出 - STM32MP157_PRO - 嵌入式开发问答社区
3.4 第一个驱动
怎么编写驱动程序
① 确定主设备号,也可以让内核分配
② 定义自己的 file_operations 结构体
③ 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
④ 把 file_operations 结构体告诉内核: register_chrdev
⑤ 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
⑥ 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
⑦ 其他完善:提供设备信息,自动创建设备节点: class_create,device_create
解释:需要实现驱动程序对应的open/write/read等函数,将这些函数放在file_operations 结构体里面,再将这个结构体注册到内核里面(register_chrdev函数),注册到什么地方呢,由主设备号区分(类似一个数组chrdevs[主设备号])。入口函数调用注册函数;有入口就有出口函数(卸载驱动程序)。
应用程序调用open函数打开一个文件"/dev/xxx",最终得到一个整数(文件描述符),这个整数对应内核中的一个结构体struct file
lag、mode就会保存在这个结构体的这两个参数中,还有一个f_op属性,里面有read/write/open等函数
应用程序打开某个设备节点时/dec/xxx,会根据设备节点的主设备号,在内核的chrdevs数组中,找到file_operation结构体,这个结构体中提供了驱动程序的read/write/open等函数。
参考 driver/char 中的程序,包含头文件,写框架,传输数据:
-
驱动中实现 open, read, write, release, APP 调用这些函数时,都打印内核信息
-
APP 调用 write 函数时,传入的数据保存在驱动中
-
APP 调用 read 函数时,把驱动中保存的数据返回给 APP
放到ubuntu的/home/book/nfs_rootf/01hello_drv下
hello_drv.c
主要还是围绕
① 确定主设备号,也可以让内核分配
② 定义自己的 file_operations 结构体
③ 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
④ 把 file_operations 结构体告诉内核: register_chrdev
⑤ 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
⑥ 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
⑦ 其他完善:提供设备信息,自动创建设备节点: class_create,device_create
/*************************************************************************> File Name: hello.drv.c> Author: Winter> Created Time: Sun 07 Jul 2024 12:35:19 AM EDT************************************************************************/#include <linux/module.h>#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>// 1确定主设备号,也可以让内核分配
static int major = 0; // 让内核分配
static char kernel_buf[1024]; // 保存应用程序的数据
static struct class *hello_class;#define MIN(a, b) (a < b ? a : b)// 3 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);// 将kernel_buf区的数据拷贝到用户区数据buf中,即从内核kernel_buf中读数据err = copy_to_user(buf, kernel_buf, MIN(1024, size));return MIN(1024, size);
}static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);// 把用户区的数据buf拷贝到内核区kernel_buf,即向写到内核kernel_buf中写数据err = copy_from_user(kernel_buf, buf, MIN(1024, size));return MIN(1024, size);
}static int hello_drv_open (struct inode *node, struct file *file)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}static int hello_drv_close (struct inode *node, struct file *file)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}// 2定义自己的 file_operations 结构体
static struct file_operations hello_drv = {.owner = THIS_MODULE,.open = hello_drv_open,.read = hello_drv_read,.write = hello_drv_write,.release = hello_drv_close,
};// 4把 file_operations 结构体告诉内核: register_chrdev
// 5谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
static int __init hello_init(void)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);// 注册hello_drv,返回主设备号major = register_chrdev(0, "hello", &hello_drv); /* /dev/hello */// 创建classhello_class = class_create(THIS_MODULE, "hello_class");err = PTR_ERR(hello_class);if (IS_ERR(hello_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "hello");return -1;}// 创建devicedevice_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */return 0;
}// 6有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
static void __exit hello_exit(void)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);device_destroy(hello_class, MKDEV(major, 0));class_destroy(hello_class);// 卸载unregister_chrdev(major, "hello");
}// 7其他完善:提供设备信息,自动创建设备节点: class_create,device_create
module_init(hello_init);
module_exit(hello_exit);MODULE_LICENSE("GPL");
测试程序:hello_drv_test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>/** ./hello_drv_test -w abc* ./hello_drv_test -r*/
int main(int argc, char **argv)
{int fd;char buf[1024];int len;/* 1. 判断参数 */if (argc < 2) {printf("Usage: %s -w <string>\n", argv[0]);printf(" %s -r\n", argv[0]);return -1;}/* 2. 打开文件 */fd = open("/dev/hello", O_RDWR);if (fd == -1){printf("can not open file /dev/hello\n");return -1;}/* 3. 写文件或读文件 */if ((0 == strcmp(argv[1], "-w")) && (argc == 3)){len = strlen(argv[2]) + 1;len = len < 1024 ? len : 1024;write(fd, argv[2], len);}else{len = read(fd, buf, 1024); buf[1023] = '\0';printf("APP read : %s\n", buf);}close(fd);return 0;
}
Makefile:换成自己的内核
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册KERN_DIR = /home/book/100ask_stm32mp157_pro-sdk/Linux-5.4all:make -C $(KERN_DIR) M=`pwd` modules$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.cclean:make -C $(KERN_DIR) M=`pwd` modules cleanrm -rf modules.orderrm -f hello_drv_testobj-m += hello_drv.o
编译
因为重新编译安装了内核,所以要在板子上重新挂载
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs/ /mnt
装载驱动程序
insmod hello_drv.ko
cat /proc/devices
lsmod
执行测试程序
4 Hello 驱动中的一些补充知识
4.1 module_init/module_exit 的实现
一个驱动程序有入口函数、出口函数,代码如下
module_init(hello_init);
module_exit(hello_exit);
驱动程序可以被编进内核里,也可以被编译为 ko 文件后手工加载。 对于这两种形式,“ module_init/module_exit”这 2 个宏是不一样的。 在内核文件“ include\linux\module.h”中可以看到这 2 个宏:
/*** module_init() - driver initialization entry point* @x: function to be run at kernel boot time or module insertion** module_init() will either be called during do_initcalls() (if* builtin) or at module insertion time (if a module). There can only* be one per module.*/
#define module_init(x) __initcall(x);/*** module_exit() - driver exit entry point* @x: function to be run when driver is removed** module_exit() will wrap the driver clean-up code* with cleanup_module() when used with rmmod when* the driver is a module. If the driver is statically* compiled into the kernel, module_exit() has no effect.* There can only be one per module.*/
#define module_exit(x) __exitcall(x);
具体的
/* Each module must use one module_init(). */
#define module_init(initfn) \static inline initcall_t __maybe_unused __inittest(void) \{ return initfn; } \int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \static inline exitcall_t __maybe_unused __exittest(void) \{ return exitfn; } \void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
编译驱动程序时,我们执行“ make modules”这样的命令,它在编译 c 文件时会定义宏 MODULE