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