【IMX6ULL驱动开发学习】05.IMX6ULL驱动开发_编写第一个hello驱动【熬夜肝】

news2024/11/30 12:33:41

经过以下四个步骤,终于可以开始驱动开发了

01.安装交叉编译环境【附下载地址】
02.IMX6ULL烧写Linux系统
03.设置IMX6ULL开发板与虚拟机在同一网段
04.IMX6ULL开发板与虚拟机互传文件

目录

一、获取内核、编译内核
二、创建vscode工作区,添加内核目录和个人目录
三、了解驱动程序编写流程
四、第一个驱动程序 - hello驱动
五、IMX6ULL验证hello驱动

一、获取内核、编译内核

1、获取内核文件

获取Linux内核文件,可以从Linux Kernel官网下载,我这里为了跟开发板中的系统一致,避免出现其他问题,所以使用的韦东山老师提供的Linux-4.9.88内核文件,需要自取

链接:https://pan.baidu.com/s/111M2FsgJXAPsQ3ppeVwbFQ
提取码:p7wp

2、编译内核文件
为什么要编译内核文件,因为驱动代码的编译要基于编译好的内核文件的
在编译之前,要在~/.bashrc文件下添加两行内容,来指定编译的平台和工具链

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-

编译内核步骤:(如果中途报错了,自行百度一下就行,网上很多解决办法的,这里就不一一列举了)

  1. 先删除之前编译所生成的文件和配置文件,备份文件等
make mrproper

成功现象:无内容输出

  1. 设置内核的相关配置
make 100ask_imx6ull_defconfig

成功现象:

HOSTCC scripts/basic/fixdepHOSTCC scripts/kconfig/conf.o
SHIPPED scripts/kconfig/zconf.tab.c
SHIPPED scripts/kconfig/zconf.lex.c
SHIPPED scripts/kconfig/zconf.hash.c
HOSTCCscripts /kconfig/zconf.tab.o
HOSTLDscripts/kconfig/conf
#
#configuration written to .config
#
  1. 生成镜像文件(-j4会快一些,代表使用4个核心一起编译,具体用几个核心根据自己虚拟机具体情况而定)
make zImage -j4

成功现象:在 内核文件/arch/arm/boot/目录下 生成 zImage 文件,且没有报错

  1. 生成设备树(这一步也做一下,很快的,虽然我们用不到,就跟着步骤来嘛)
make dtbs

成功现象:输出几行内容,无报错

  1. 在家目录下新建 nfs_rootfs 目录,将镜像文件、生成的设备树拷贝其目录下 (可不做)
cp arch/arm/boot/zImage ~/nfs_rootfs
cp arch/arm/boot/dts/100ask_imx6ull-14x14.dtb ~/nfs_rootfs
  1. 编译内核模块
make modules

成功现象:输出很多.o文件,最后输出一些.ko文件,无报错


二、配置vscode,添加内核目录和个人目录

  1. 使用vscode打开linux-4.9.88内核目录,点击左上角 “文件” ,“将工作区另存为”,选择家目录下,随便起个名字
  2. 使用vscode打开自己要编写驱动的文件目录,右键自己的文件夹,选择“ 将文件夹添加到工作区”,选择自己创建的那个工作区
  3. 至此就可以在vscode同一个工作区下看到自己的驱动文件和linux内核文件了,方便后续独照内核文件内容

在这里插入图片描述

  1. 在.vscode 下的c_cpp_properties.json 文件中设置工作路径
"/home/me/Linux-4.9.88/tools/virtio",
"/home/me/Linux-4.9.88/include/**",
"/home/me/Linux-4.9.88/include/linux/**",
"/home/me/Linux-4.9.88/arch/arm/include/**",
"/home/me/Linux-4.9.88/arch/arm/include/generated/**"

三、了解驱动程序编写流程

1. 先看内核目录下原有的驱动是怎么写的
打开Linux-4.9.88/drivers/char目录(看名字就猜到该目录下存放应该是字符驱动代码
发现有个 ds1602.c ,打开看看它是怎么写的(因为我学过了,选择这个文件,展示主要代码,有助于入门理解)

#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/capability.h>
#include <linux/init.h>
#include <linux/mutex.h>

#include <mach/hardware.h>
#include <asm/mach-types.h>
#include <asm/uaccess.h>
#include <asm/therm.h>

static DEFINE_MUTEX(ds1620_mutex);
static const char *fan_state[] = { "off", "on", "on (hardwired)" };

.....
..... 省略
.....

static int __init ds1620_init(void)
{
	int ret;
	struct therm th, th_start;

	if (!machine_is_netwinder())
		return -ENODEV;

	ds1620_out(THERM_RESET, 0, 0);
.....
..... 省略
.....

static int ds1620_open(struct inode *inode, struct file *file)
{
	return nonseekable_open(inode, file);
}

.....
..... 省略
.....

static ssize_t ds1620_read(struct file *file, char __user *buf, size_t count, loff_t *ptr)
{
	signed int cur_temp;
	signed char cur_temp_degF;

	cur_temp = cvt_9_to_int(ds1620_in(THERM_READ_TEMP, 9)) >> 1;

	/* convert to Fahrenheit, as per wdt.c */
	cur_temp_degF = (cur_temp * 9) / 5 + 32;

	if (copy_to_user(buf, &cur_temp_degF, 1))
		return -EFAULT;

	return 1;
}


.....
..... 省略
.....

static const struct file_operations ds1620_fops = {
	.owner		= THIS_MODULE,
	.open		= ds1620_open,
	.read		= ds1620_read,
	.unlocked_ioctl	= ds1620_unlocked_ioctl,
	.llseek		= no_llseek,
};

.....
..... 省略
.....

static void __exit ds1620_exit(void)
{
#ifdef THERM_USE_PROC
	remove_proc_entry("therm", NULL);
#endif
	misc_deregister(&ds1620_miscdev);
}

module_init(ds1620_init);
module_exit(ds1620_exit);

MODULE_LICENSE("GPL");

2. 驱动程序主要构成(主要是的前面四个)
· file_operations 结构体 : 为系统调用提供驱动程序入口的结构体
· module_init : 定义驱动模块的入口函数
· module_exit : 定义驱动模块的退出函数
· MODULE_LICENSE(“GPL”) : 声明模块许可证,指明这是GNU General Public License的任意版本,
                                                     否则在加载此模块时,会收到内核被污染 “kernel tainted” 的警告

· ds1620_init :ds1602初始化函数
· ds1620_open : 打开ds1602设备函数
· ds1620_read : 读ds1602设备函数
· ds1620_exit : ds1602退出驱动函数

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

3. 驱动程序实现过程及调用原理
· module_init 指定驱动入口函数
· 设备对象通过 struct file_operations 结构体定义
· 入口函数中调用 register_chrdev 函数,传入注册定义好的驱动设备变量,生成设备号
· 在开发板上通过 insmod 命令将编译好的驱动模块(.ko文件,要从虚拟机传到板子上哦)载入内核
· 通过应用程序可以对设备进行读写等操作,读写等操作通过系统调用 register_chrdev 结构体中指定的驱动模块读写等函数来实现
· close设备时,系统调用 module_exit 指定的驱动模块退出函数

以上便是驱动程序实现过程及应用程序调用驱动程序的原理(按照个人理解写的,大概是这么个流程,有不恰当的地方欢迎指出)

四、第一个驱动程序 - hello驱动

1. 在个人目录下新建 hello_drv.c 文件
2. 照葫芦画瓢,把上面 ds1602 用到的
头文件**都复制过来
3. 继续照葫芦画瓢编写 file_operations 结构体**

static const struct file_operations hello_drv = {
    .owner 		= THIS_MODULE,
	.read		= hello_read,
	.write		= hello_write,
	.open		= hello_open,
	.release    = hello_release,
};

假装我们的驱动也可以读、写,当然也必须有open和release(就是close)
当然 .owner = THIS_MODULE, 也必须有,原因见博客https://blog.csdn.net/a954423389/article/details/6101369

4. 研究file_operations结构体
每个函数都有对应的模板,可不是乱写的,因为这些函数组中都会被系统调用,参数都是固定的,可以按住ctrl键,用鼠标点击file_operations,跳转到该结构体定义处,可以看到每个函数指针的形式
在这里插入图片描述

5. 照葫芦画瓢实现hello_read、hello_write、hello_open、hello_release函数

/*养成好习惯,驱动程序都加static修饰*/
static int hello_open (struct inode *node, struct file *filp)
{
	printk("hello_open\n");
	printk("%s %s %d\n",__FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static ssize_t hello_read (struct file *filp, char *buf, size_t size, loff_t *offset)
{
	printk("hello_read\n");
	return size;  //返回读取字节数
}

static ssize_t hello_write (struct file *filp, const char *buf, size_t size, loff_t *offset)
{
	printk("hello_write\n");
	return size;  //返回写入字节数
}

static int hello_release (struct inode *node, struct file *filp)
{
	printk("hello_release\n");
	return 0;
}

6. 编写hello驱动的入口函数、出口函数
入口函数需要用到 register_chrdev 函数
出口函数(退出函数)需要用到 unregister_chrdev 函数,但是 ds1602.c 中没有这两个函数
没关系,我们在vscode中搜索 register_chrdev, 随便点一个看一下,研究一下用法(实在看不懂百度一下哈哈)
也可以用linux命令查找(在内核的drivers/char目录下查找)

grep "register_chrdev" * -nwr

在这里插入图片描述
我们用vscode找一下

在这里插入图片描述
发现 register_chrdev 函数需要三个参数
第一个参数是主设备号,0代表动态分配
第二个参数是设备的名字(自定义)
第三个参数是struct file_operations结构体类型的指针,代表申请设备的操作函数

同理,出口函数 unregister_chrdev
第一个参数是设备号
第二个参数是设备名称

照葫芦画瓢开始写

/*入口函数*/
static int major;
static int hello_init(void)
{
	/*返回设备号,定义设备名称为hello_drv*/
	major = register_chrdev(0,"hello_drv",&hello_drv);
	return 0;
}

/*退出函数*/
static int hello_exit(void)
{
	unregister_chrdev(major,"hello_drv");
	return 0;
}	

7. module_init 、module_exit和声明许可证

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

整个 hello_drv.c 代码

#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/capability.h>
#include <linux/init.h>
#include <linux/mutex.h>
#include <asm/mach-types.h>
#include <asm/uaccess.h>
#include <asm/therm.h>

static int major;

static int hello_open (struct inode *node, struct file *filp)
{
	printk("hello_open\n");
	printk("%s %s %d\n",__FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static ssize_t hello_read (struct file *filp, char *buf, size_t size, loff_t *offset)
{
	printk("hello_read\n");
	return size;  //返回读取字节数
}

static ssize_t hello_write (struct file *filp, const char *buf, size_t size, loff_t *offset)
{
	printk("hello_write\n");
	return size;  //返回写入字节数
}

static int hello_release (struct inode *node, struct file *filp)
{
	printk("hello_release\n");
	return 0;
}

/*1.定义 file_operations 结构体*/
static const struct file_operations hello_drv = {
    .owner 		= THIS_MODULE,
	.read		= hello_read,
	.write		= hello_write,
	.open		= hello_open,
	.release    = hello_release,
};


/*2.register_chrdev*/

/*3.入口函数*/
static int hello_init(void)
{
	//设备号
	major = register_chrdev(0,"hello_drv",&hello_drv);
	return 0;
}

/*4.退出函数*/
static int hello_exit(void)
{
	unregister_chrdev(0,"hello_drv");
	return 0;
}	

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

8. 编写Makefile

不多说,直接上代码

KERN_DIR = /home/me/Linux-4.9.88

PWD ?= $(shell KERN_DIR)

all:	
	make -C $(KERN_DIR) M=$(PWD) modules
	#$(CROSS_COMPILE)gcc -o hello_test hello_test.c
clean:
	make -C $(KERN_DIR) M=$(PWD) modules clean
	rm -rf modules.order
	rm -f hello_drv

obj-m += hello_drv.o

KERN_DIR = /home/me/Linux-4.9.88 : 编译程序的依赖目录

9. make一下,生成.ko文件

me@ubuntu:~/Linux_ARM/IMX6ULL/hello_driver$ ls
hello_drv.c  hello_drv.ko  hello_drv.mod.c  hello_drv.mod.o  hello_drv.o  
hello_test  hello_test.c  Makefile  modules.order  Module.symvers

主要就是.ko文件,就是加载到内核里的驱动模块文件


五、IMX6ULL验证hello驱动

1. 将虚拟机编译生成的.ko文件拷贝到IMX6ULL开发板上
这步操作需要虚拟机和开发板在同一网段下,可以借鉴以下两篇博客
03.设置IMX6ULL开发板与虚拟机在同一网段
04.IMX6ULL开发板与虚拟机互传文件
我采用的是NFS挂载的方式

mount -t nfs -o nolock,vers=3 192.168.1.200:/home/me/Linux_ARM/IMX6ULL/hello_driver /mnt

在这里插入图片描述
如果出现报错

failed: Device or resource busy

执行

unmount /mnt

2.加载内核
执行命令加载内核

insmod hello_drv.ko
[root@100ask:/mnt]# insmod hello_drv.ko 
[   80.794911] hello_drv: loading out-of-tree module taints kernel.
[root@100ask:/mnt]#

显示已载入系统的模块

lsmod

在这里插入图片描述
查看 hello_drv.ko 驱动模块的设备号

cat /proc/devices
226 drm
240 hello_drv
241 adxl345
242 spidevx
243 irda
244 dht11

可以看到hello驱动的设备号是240

3.生成设备节点

mknod /dev/hello c 240 0

/dev/hello : 生成设备节点的名称
c :说明是字符设备
240 : 主设备号
0 : 子设备号(不指定子设备号,为0)

这时候查看hello设备也可以用下面的命令

[root@100ask:/mnt]# ls /dev/hello -l
crw-r--r-- 1 root root 240, 0 Jan  1 11:34 /dev/hello

4.编写应用程序验证驱动

PS:在第四节第8小节中,将 $(CROSS_COMPILE)gcc -o hello_test hello_test.c 这一行注释去掉

编写 hello_test.c 应用程序

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>


int main(int argc, char *argv[])
{
    if(argc < 2){
        printf("please input  at least 2 args\n");
        printf("%s <dev> [string]\n", argv[0]);
        return -1;
    }

    /*open*/
    int fd;
    fd = open(argv[1], O_RDWR);
    if(fd < 0){
        printf("open failed\n");
        return -2;
    }

    /*read*/
    if(argc == 2){
        printf("read operation\n");
        return 2;
    }

    /*write*/
    if(argc == 3){
        printf("write operation : %s \n", argv[2]);
        return 3;
    }

    close(fd);

    return 0;
}

该程序的使用方式:

./hello_test   /dev/hello  123abc     两个参数:模拟写操作
./hello_test   /dev/hello             一个参数:模拟读操作

/dev/hello 是我们要打开的设备名称,在应用程序中,使用 open 函数打开,使用 close 函数关闭

如果打开成功,则系统会调用 hello 驱动的 open 函数,我们就会看到相应的打印信息(打印出当前文件名,函数名,行数)

static int hello_open (struct inode *node, struct file *filp)
{
	printk("hello_open\n");
	printk("%s %s %d\n",__FILE__, __FUNCTION__, __LINE__);
	return 0;
}

注意驱动程序中使用的都是printk函数,该函数是内核调用的,想要在开发板的串口打印信息中看到输出,需要执行以下命令

echo "7 4 1 7" > /proc/sys/kernel/printk

测试驱动写操作,成功!!!

[root@100ask:/mnt]# ./hello_test /dev/hello 
[  499.512588] hello_open
[  499.516872] /home/me/Linux_ARM/IMX6ULL/hello_driver/hello_drv.c hello_open 28
[  499.525082] hello_read
read operation [  499.528427] hello_release

[root@100ask:/mnt]# 

测试驱动读操作,成功!!!

[root@100ask:/mnt]# ./hello_test /dev/hello abc123
[  500.725340] hello_open
[  500.727762] /home/me/Linux_ARM/IMX6ULL/hello_driver/hello_drv.c hello_open 28
[  500.736217] hello_write
write length = 6 [  500.739735] hello_release

[root@100ask:/mnt]# 

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/625835.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《计算机组成原理》期末考试手写笔记——模块五: 并行主存系统(交叉存储器+顺序存储器“带宽”的计算方法)

目录 &#xff08;一&#xff09;知识点总结 &#xff08;二&#xff09;经典考试例题 1.设主存储器容量为256字&#xff0c;字长为32位&#xff0c;模块数m4&#xff0c;分别用顺序方式和交叉方式进行组织。主存储器的存储周期T200ns&#xff0c;数据总线宽度为32位&#x…

EMQ X(2):EMQ X服务端环境搭建与配置

1 安装 EMQ X 目前支持的操作系统: Centos6Centos7OpenSUSE tumbleweedDebian 8Debian 9Debian 10Ubuntu 14.04Ubuntu 16.04Ubuntu 18.04macOS 10.13macOS 10.14macOS 10.15Windows Server 2019 产品部署建议 Linux 服务器&#xff0c;不推荐 Windows 服务器。 安装的方式有…

【Linux】HTTP协议

目录 &#x1f680;前言&#x1f683;HTTP协议 &#x1f684;1、URL网址&#x1f685;2、URL的编码和解码&#x1f687;3、HTTP协议格式&#x1f688;4、HTTP请求&#x1f689;4.1、 HTTP GET和POST方法&#x1f68b;4.2、HTTP状态码&#x1f68a;4.3、HTTP常见Header &#x1…

redis架构设计: redis-server的启动(硬核分析)

怎么在windows上用clion搭建redis的源码阅读环境 请看我的上一篇文章 redis启动之后都干了什么呢? 我们知道&#xff0c;redis的服务端对应的源码位置是server.c main函数是程序启动的入口 &#xff0c;下面我来一行一行的分析server.c的源码 1、定义时间函数变量 struct …

chatgpt赋能python:Python多种输出格式详解

Python多种输出格式详解 对于Python程序员来说&#xff0c;输出是非常重要的。无论是在开发阶段还是在生产环境中&#xff0c;输出都是我们调试程序和确认程序运行是否正常的重要手段。Python标准库提供了丰富的输出格式&#xff0c;本文介绍了几种常见的输出格式及其使用方法…

因为写不出拖拽移动效果,我恶补了一下Dom中的各种距离

目录 背景 JS Dom各种距离释义 第一个发现 window.devicePixelRatio 的存在 document.body、document.documentElement和window.screen的宽高区别 scrollWidth, scrollLeft, clientWidth关系 元素自身和父级元素的scrollWidth和scrollLeft关系? offsetWidth和clientWid…

【FDA】图像通过傅里叶变换改变光谱风格,实现域自适应

FDA: Fourier Domain Adaptation for Semantic Segmentation, CVPR2020 翻译&#xff1a;CVF2020邻域自适应/语义分割&#xff1a;FDA: Fourier Domain Adaptation for Semantic SegmentationFDA&#xff1a;用于语义分割的傅立叶域自适应算法_傅里叶域适应_HheeFish的博客-CS…

【TCP/IP】多进程服务器的实现(进阶) - 进程和僵尸进程

目录 僵尸(Zombie)进程 僵尸进程的产生机制 僵尸进程的危害 僵尸进程的销毁 wait函数 waitpid函数 进程管理在网络编程中十分重要&#xff0c;如果未处理好&#xff0c;将会导致出现“僵尸进程”&#xff0c;进而影响服务器端对进程的管控。 僵尸(Zombie)进程 第一次听到…

数据类型

常见的数据类型&#xff1a; int&#xff0c;整数类型&#xff08;整形&#xff09;bool&#xff0c;布尔类型str&#xff0c;字符串类型list&#xff0c;列表类型tuple&#xff0c;元组类型dict&#xff0c;字典类型set&#xff0c;集合类型float&#xff0c;浮点类型&#x…

python Web开发 flask轻量级Web框架实战项目--学生管理系统

上次发的一篇文章&#xff0c;有很多朋友私信我要后面的部分&#xff0c;那咱们就今天来一起学习一下吧&#xff0c;因为我的数据库这门课选中的课题是学生管理系统&#xff0c;所以今天就以这个课题为例子&#xff0c;从0到1去实现一个管理系统。数据库设计部分我会专门出一个…

《Java 核心技术面试》课程笔记(十二)

Java 有几种文件拷贝方式&#xff1f;哪一种最高效&#xff1f; 典型回答 Java 有多种比较典型的文件拷贝实现方式&#xff0c;比如&#xff1a;利用java.io 类库&#xff0c;直接为源文件构建一个 FileInputStream 读取&#xff0c;然后再为目标文件构建一个 FileOutputStre…

chatgpt赋能python:Python模块(Module)是什么?

Python模块&#xff08;Module&#xff09;是什么&#xff1f; Python模块&#xff08;Module&#xff09;是指一些预先编写好的代码&#xff0c;这些代码可以在程序中被引入和使用。它们可以包含可以复用的函数、常量和类。Python模块是一种封装程序代码的方法。 下载Python…

领取的AWS亚马逊云服务器到期会扣费的问题解决办法。

本篇文章主要讲解&#xff0c;领取的AWS亚马逊服务器到期后会持续扣费问题的解决办法。 作者&#xff1a;任聪聪 日期&#xff1a;2023年6月8日 关于aws服务器一年免费期限到期后扣费的问题&#xff0c;网络上的文章并不是很全&#xff0c;故此我通过个人的经验进行了如下的教程…

chatgpt赋能python:Python怎么print换行?

Python怎么print换行&#xff1f; 如果你是一个Python开发者&#xff0c;你可能遇到过需要在Python中打印输出换行的情况。本文将分享几种方式&#xff0c;让你学会如何在Python中print换行。 1. 使用"\n" 您可以在print语句中使用"\n"来表示换行。这个…

陈丹琦团队新作:单卡A100可训300亿参数模型啦!

夕小瑶科技说 原创 作者 | 智商掉了一地、ZenMoore 近年来&#xff0c;随着大模型的涌现&#xff0c;微调语言模型已经在各种下游任务上展现出了卓越的性能。然而&#xff0c;这些庞大模型的参数量常常达到数十亿甚至上百亿的级别&#xff0c;训练这样规模的模型需要消耗大量…

chatgpt赋能python:Python视图(View)在SEO中的重要性

Python视图&#xff08;View&#xff09;在SEO中的重要性 什么是Python视图&#xff1f; Python视图是指&#xff0c;在Web应用程序中&#xff0c;将业务逻辑与显示逻辑分开处理&#xff0c;并以代码的形式定义的可重用组件。它们是与URL相对应的函数或方法。Python视图可以生…

uniapp:uni-app-base 项目基础配置,开箱可用

目前&#xff08;20230605&#xff09;uni-app最新版本&#xff08;3.8.4.20230531&#xff09; 一、官网文档 uni-app官网 二、创建项目 项目目标&#xff1a;vue3tsvitevscode 创建以 typescript 开发的工程&#xff08;如命令行创建失败&#xff0c;请直接访问 gitee 下…

《面试1v1》JVM调优

我是 javapub&#xff0c;一名 Markdown 程序员从&#x1f468;‍&#x1f4bb;&#xff0c;八股文种子选手。 《面试1v1》 连载中… 面试官&#xff1a; 小伙子,说听说你JVM调优挺在行? 候选人&#xff1a; 谢谢夸奖,我对JVM调优还在学习中,远未达到在行的程度。不过日常工作…

springboot项目外卖管理 day04-文件的上传下载与菜品的新增与修改

文章目录 1、文件上传下载1.1、文件上传介绍与实现1.2、文件下载介绍与实现 2、新增菜品2.1、需求分析2.2、代码开发-梳理交互过程2.2.1、菜品分类下拉框&#xff1a;在CategoryController添加 2.2.2、新增保存 3、菜品信息分页查询3.1、代码开发-梳理交互过程对象拷贝BeanUtil…

chatgpt赋能python:Python输出方法详解:从基础print()到高级logging模块

Python输出方法详解&#xff1a;从基础print()到高级logging模块 在Python编程中&#xff0c;输出是一个必不可少的步骤。然而&#xff0c;Python提供了多种输出方法&#xff0c;如何选择最适合的方法呢&#xff1f;本文将详细介绍Python输出的不同方法&#xff0c;并给出实际…