RaspberryPi系统学习

C语言编译

什么是编译

C语言的编译过程就是把我们可以理解的高级语言代码转换为计算机可以理解的机器代码的过程,其实就是一个翻译的过程。

image-20230726215712013

C 语言的编译过程包括四个步骤:

  1. 预处理
  2. 编译
  3. 汇编
  4. 连接

下面这张图就是C程序编译的完整过程

image-20230726215736431

接下我们看看编译过程不同阶段都在做什么。

1.预处理
编译过程的第一步预就是预处理,与处理结束后会产生一个后缀为(.i)的临时文件,这一步由预处理器完成。预处理器主要完成以下任务。

  • 删除所有的注释
  • 宏扩展
  • 文件包含

预处理器会在编译过程中删除所有注释,因为注释不属于程序代码,它们对程序的运行没有特别作用。

宏是使用 #define 指令定义的一些常量值或表达式。宏调用会导致宏扩展。预处理器创建一个中间文件,其中一些预先编写的汇编级指令替换定义的表达式或常量(基本上是匹配的标记)。为了区分原始指令和宏扩展产生的程序集指令,在每个宏展开语句中添加了一个“+”号。

文件包含
C语言中的文件包含是在预处理期间将另一个包含一些预写代码的文件添加到我们的C程序中。它是使用#include指令完成的。在预处理期间包含文件会导致在源代码中添加文件名的全部内容,从而替换#include<文件名>指令,从而创建新的中间文件。

2.编译
C 中的编译阶段使用内置编译器软件将 (.i) 临时文件转换为具有汇编级指令(低级代码)的汇编文件 (.s)。为了提高程序的性能,编译器将中间文件转换为程序集文件。
汇编代码是一种简单的英文语言,用于编写低级指令(在微控制器程序中,我们使用汇编语言)。整个程序代码由编译器软件一次性解析(语法分析),并通过终端窗口告诉我们源代码中存在的任何语法错误警告
下图显示了编译阶段如何工作的示例。

3.组装
使用汇编程序将程序集级代码(.s 文件)转换为机器可理解的代码(二进制/十六进制形式)。汇编程序是一个预先编写的程序,它将汇编代码转换为机器代码。它从程序集代码文件中获取基本指令,并将其转换为特定于计算机类型(称为目标代码)的二进制/十六进制代码。
生成的文件与程序集文件同名,在 DOS 中称为扩展名为 .obj 的对象文件,在 UNIX 操作系统中扩展名为 .o
下图显示了组装阶段如何工作的示例。程序集文件 hello.s 将转换为具有相同名称但扩展名不同的对象文件 hello.o。

4. 链接
链接是将库文件包含在我们的程序中的过程。库文件是一些预定义的文件,其中包含机器语言中的函数定义,这些文件的扩展名为.lib。一些未知语句写入我们的操作系统无法理解的对象 (.o/.obj) 文件中。你可以把它理解为一本书,里面有一些你不知道的单词,你会用字典来找到这些单词的含义。同样,我们使用库文件来为对象文件中的一些未知语句赋予意义。链接过程会生成一个可执行文件,其扩展名为 .exe 在 DOS 中为 .out,在 UNIX 操作系统中为 .out

举例

接下来,我们通过一个例子详细看看C编译过程中涉及的所有步骤。第一步先写一个简单的C程序并保存为hello.c

// Simple Hello World program in C
#include<stdio.h>
int main()
{
    // printf() is a output function which prints
    // the passed string in the output console
    printf("Hello World!");
    
    return 0;
}

接着我们执行编译命令对hello.c进行编译:

gcc -save-temps hello.c -o compilation

-save-temps 选项会保留所有编译过程中产生的中间文件,总共会生成四个文件。

  • hello.i 预处理器产生的文件
  • hello.s 编译器编译后产生的文件
  • hello.o 汇编程序翻译后的目标文件
  • hello.exe 可执行文件(Linux系统会产生hello.out文件)

首先,我们的C程序的预处理开始,注释从程序中删除,因为该程序中没有宏指令,因此宏扩展不会发生,我们还包含了一个stdio.h头文件,并且在预处理期间,标准输入/输出函数(如printf(),scanf()等)的声明被添加到我们的C程序中。

img

结论

  • C中的编译过程也称为将人类可理解代码(C程序)转换为机器可理解代码(二进制代码)的过程。
  • C语言的编译过程包括四个步骤:预处理、编译、汇编和链接。
  • 预处理器执行删除注释、宏扩展、文件包含。这些命令在编译过程的第一步执行。
  • 编译器可以提高程序的性能,并将中间文件转换为汇编文件。
  • 汇编程序有助于将汇编文件转换为包含机器代码的对象文件。
  • 链接器用于将库文件与对象文件链接。这是编译中生成可执行文件的最后一步。

Raspberry Pi

image-20230721214233979

树莓派3B

树莓派设置菜单:

sudo raspi-config

入设置界面

网络配置选项

image-20230727200837516

image-20230727200902580

添加开机指定ip脚本:

vim /etc/rc.local

添加:

ifconfig wlan0 192.168.27.222

image-20230727201001766

sudo systemctl restart xrdp开启远程桌面连接

如何在Ubuntu 20.04 上安装 Xrdp 服务器(远程桌面)-阿里云开发者社区 (aliyun.com)

解决vim右键粘贴问题:

image-20230727221732805

linux库详解

Linux系统通常把库文件存放在/usr/lib或/lib目录下,
Linux库文件名组成:前缀lib + 库名 + 后缀(3部分组成)
动态库:以.so作为后缀
静态库:通常以.a.la作为后缀。

动态库和静态库的区别

  1. 载入顺序不同
    静态库的代码在编译时就拷贝到应用程序中,因此当多个应用程序同时引用一个静态库函数时,内存中将会调用函数的多个副本。其优点是节省编译时间。
    动态库是在程序开始运行后且调用库函数时才被载入,被调函数在内存中只有一个副本,并且动态库可以在程序运行期间释放动态库所占用的内存。
  2. 大小与共享的差异
    静态链接库就是在程序编译的时候就被加载进来,这样的可执行文件会比较大一些,还不能共享 ;动态链接库是在程序执行的时候加载,可共享 。
  3. 库函数调用的差异
    静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。
    动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在linux的管理下,才在应用程序与相应的.so之间建立链接关系。当要执行所调用动态库中的函数时,根据链接产生的重定位信息,才转去执行动态库中相应的函数代码。

image-20230726215258543

linux静态库的生成:

gcc –o mylib.o –c mylib.c #-c是只编译而不生成可执行文件,-o指定文件名

ar rcs libmylib.a mylib.o #将目标文件加入到静态库中,静态库为libmylib.a
参数说明:
/* r —在库中加入新成员文件,如果要加入的成员文件存在,则替换之默认情况下,新的成员文件增加在库的结尾处
/* c 创建一个库
/* s 无论ar命令是否修改了库内容,都强制重新生成库符号表

cp libmylib.a /usr/lib/libmylib.a #将静态库拷贝到 linux 的库目录(/usr/lib或/lib)下
#如果没有移动文件到/usr/lib里
#编译时:
gcc –o test test.c –lmylib -L./ #链接静态库来编译(优先在该目录下寻找)

gcc –o test test.c –lmylib #链接静态库来编译

到这里静态库就生成完了,下面我们尝试用一个程序链接它。

linux动态库的生成:

  • 在Linux环境下,只要在编译函数库源程序时加上
    -fPIC -shared选项即可生成动态链接库。
1. 动态库的创建
 gcc –fPIC -o mylib.o -c mylib.c (生成符号表)
 gcc –shared -o libmylibs.so mylib.o(防止重复加载)

· 或者可以直接使用一条命令
 gcc –fPIC –shared –o libmylibs.so mylib.c (可以省略 -c 和 .o后缀文件名)

· 将生成的静态库拷贝至/usr/lib或/lib目录中
 cp libmylibs.so /usr/lib/libmylibs.so

动态库的使用

(1)通过gcc命令调用
# gcc –o test test.c -lmylibs

(2)通过调用系统函数来使用动态链接库

在这里插入图片描述

其中:
dlopen函数的参数flag可取的值有:RTLD_LAZY、 RTLD_NOW、 RTLD_GLOBAL:

RTLD_LAZY:在dlopen()返回前,对于动态库中存在的未定义的变量不执行解析,即不解析这个变量的地址
RTLD_NOW:在dlopen返回前,解析出每个未定义变量的地址,如果解析不出来,dlopen会返回NULL,错误为“Undefined symbol:”
RTLD_GLOBAL:使库中被解析出来的变量在随后的其它链接库中也可以使用,即全局有效

  1. 通过调用系统函数来使用动态链接库的实例

创建testso.c文件:

#include <stdioh>
#include <dlfcn.h>
 int main (void)
{
  void *handle;
  char *error;
  void (*welcome)(); //要变成指针函数来用(由符号表引出的指针)
if ((handle = dlopen(“/usr/lib/libmylibs.so”, RTLD_LAZY) == NULL)
  {
    printf (“dlopen error\n”);
    exit(1);
  }
welcome = dlsym(handle, “welcome”);
  if ((error = dlerror()) != NULL)
   {
     printf (“dlsym error \n”);
     exit(1);
   }
  welcome ();
  dlclose (handle);
  exit(0);
}

头文件mylib.h —— 声明静态库所导出的函数:

#ifndef  _mylib_H_     //如果没有定义此标识符,编码以下程序
#define  _mylib_H_    1 
  void welcome();
  void outstring (const char * str);
#endif

对应于头文件的源文件mylib.c —— 实现静态库所导出的函数

#include “mylib.h”
#include <stdio.h>
void welcome() {
     printf (“welcome to libmylib\n”);
}
void outstring(const char * str) {
     if (str !=NULL)
         printf (“%s”,str);
}

编译:

#gcc –o testso testso.c -ldl

l其中:-ldl 指明dlopen等函数所在的库类型

动态库链接在当前文件夹下:

方法一:在配置文件/etc/ld.so.conf中指定动态库搜索路径。
vi /etc/ld.so.conf
添加 lib目录

方法二:通过环境变量LD_LIBRARY_PATH指定动态库搜索路径。
export LD_LIBRARY_PATH=”home/pi”

第二种方法可以写一个脚本导入后再进行执行即可

其中方法三可以避免安装部署的麻烦

方法三示例:

假设main.cpp,hello.h,hello.cpp,其中main.cpp调用了hello类中的方法

1 生成hello.so

g++ -shared hello.cpp -o libhello.so

2 编译main.cpp,并链接,并指定运行时libhello.so的位置

  g++ main.cpp -lhello -L./ -Wl,-rpath=./ -o main

值得一提的是,如果采用带版本号的库,例如libhello.so.2

链接命令可使用g++ main.cpp libhello.so.2 -L./ -Wl,-rpath=./ -o main

2)加入第二个so库

g++ main.cpp -L./second/ -Wl,-rpath=./second/ -lsecond -L./hello/ -Wl,-rpath=./hello/ -lhello -o main

#在编译时加入 -Wl,-rpath=./
#-L./ 是将静态库链接到当前目录下

树莓派调试串口

树莓派wiringPi库详解 - lulipro - 博客园 (cnblogs.com)

在使用串口前需要先进行一些配置,将串口的用途更改到调试:

/* 修改 cmdline.txt文件 */
>cd /boot/
>sudo vim cmdline.txt


删除【】之间的部分
dwc_otg.lpm_enable=0 【console=ttyAMA0,115200】 kgdboc=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait
dwc_otg.lpm_enable=0 kgdboc=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait
fsck.repair=yes

/*修改 inittab文件 */
>cd /etc/
>sudo vim inittab

注释掉最后一行内容:,在前面加上 # 号
#T0:23:respawn:/sbin/getty -L ttyAMA0 115200 vt100


sudo reboot 重启

(219条消息) 树莓派 gpio / 串口通信_树莓派串口通信_aworkholic的博客-CSDN博客

配置串口输出:

树莓派包含两个串口(树莓派3及以前版本)
1.硬件串口(/dev/ttyAMA0),硬件串口由硬件实现,有单独的波特率时钟源,性能高,可靠。一般优先选择这个使用。
2.mini串口(/dev/ttyS0),mini串口时钟源是由CPU内核时钟提供,波特率受到内核时钟的影响,不稳定。

树莓派3及以前版本仅2个串口,4和400有4个串口,cm系列有6个串口,详见树莓派官网Configuring UARTs)
树莓派4开启 ttyAMA1,可以直接配置 dtoverlay=uart1 即可(默认dtoverlay=uart1,txd1_pin=32,rxd1_pin=33)。

想要通过树莓派的GPIO引脚进行稳定的串口通信,需要修改串口的映射关系。
serial0是GPIO引脚对应的串口,serial1是蓝牙对应的串口,默认未启用serial0。使用ls -l /dev/serial*查看当前的映射关系:
在这里插入图片描述

可以看到这里是,蓝牙串口serial1使用硬件串口ttyAMA0。

2开启GPIO串口功能,并使用硬件串口

使用sudo raspi-config 进入图形界面
选择菜单 Interfacing Options -> P6 Serial,
第一个选项(would you like a login shell to be accessible over serial?)选择 NO,
第二个选项(would you like the serial port hardware to be enabled?)选择 YES

保存后重启,查看映射关系
在这里插入图片描述

比之前多了一个gpio的串口serial0,并且使用的ttyS0。这里已经是开启了GPIO串口功能,但是使用的cpu实现的软件串口。

如果想使用稳定可靠的硬件串口,就要将树莓派3b+的硬件串口与mini串口默认映射对换(先禁用蓝牙 sudo systemctl disable hciuart)。

在/boot/config.txt文件末尾添加一行代码 dtoverlay=pi3-miniuart-bt (树莓派4B也使用这个命令)。 还可以直接配置禁用bluetooth,代码为 dtoverlay=disable-bt,见参考链接 【树莓派 功能配置(含网络)不定期更新】。

保存后重启再查看设备对用关系,发现已经调换成功。
在这里插入图片描述

3 禁用串口的控制台功能

前面步骤已经交换了硬件串口与mini串口的映射关系,但是,现在还不能使用树莓派串口模块与电脑进行通信,因为,树莓派gpio口引出串口默认是用来做控制台使用的,即是为了用串口控制树莓派,而不是通信。所以我们要禁用此默认设置。
首先执行命令如下:
sudo systemctl stop serial-getty@ttyAMA0.service
sudo systemctl disable serial-getty@ttyAMA0.service
然后执行命令行修改文件:
sudo nano /boot/cmdline.txt
并删除语句console=serial0,115200(没有的话就不需要此步骤)

测试代码:

借鉴一下树莓派wiringPi库详解 - lulipro - 博客园 (cnblogs.com)

  1 #include <wiringPi.h>
  2 #include <wiringSerial.h>
  3 #include <stdio.h>
  4 #include <stdlib.h>
  5
  6 int main()
  7 {
  8     int fd;
  9
 10     if(-1==wiringPiSetup())
 11     {
 12         printf("set up error\n");
 13         exit(-1);
 14     }
 15     if((fd = serialOpen("/dev/ttyAMA0",9600))==-1)    //初始化串口,波特率9600
 16     {
 17         printf("serial open error!\n");
 18     }
 19
 20     while(1)
 21     {
 22         serialPutchar(fd,'c');
 23         printf("serialputchar:c\n");
 24         delayMicroseconds(1000000);
 25     }
 26
 27     return 0;
 28 }

交叉编译

有些平台上不允许或者不能够安装编译器,所以我们需要在一个平台上编译出可执行文件上传到需要运行的目标平台上去运行。

树莓派也需要交叉编译,例如为树莓派的内核进行编译

在做实际工作之前,我想我们应该先掌握一些关于交叉编译的基本知识。

宿主机(host) :编辑和编译程序的平台,一般是基于X86的PC机,通常也被称为主机。

目标机(target):用户开发的系统,通常都是非X86平台。host编译得到的可执行代码在target上运行。

交叉编译的工具:交叉编译器

我们建议使用现成的脚本来生成交叉编译器,因为在配置交叉编译器时,会经常使用一些难以理解的开关项。

linux的软链接和硬链接

Linux 文件系统最重要的特点之一是它的文件链接。链接是对文件的引用,这样您可以让文件在文件系统中多处被看到。不过,在 Linux 中,链接可以如同原始文件一样来对待。链接可以与普通的文件一样被执行、编辑和访问。对系统中的其他应用程序而言,链接就是它所对应的原始文件。当您通过链接对文件进行编辑时,您编辑的实际上是原始文件。链接不是副本。有两种类型的链接:硬链接和符号链接软链接)。

硬链接只能引用同一文件系统中的文件。它引用的是文件在文件系统中的物理索引(也称为inode)。当您移动或删除原始文件时,硬链接不会被破坏,因为它所引用的是文件的物理数据而不是文件在文件结构中的位置。硬链接的文件不需要用户有访问原始文件的权限,也不会显示原始文件的位置,这样有助于文件的安全。如果您删除的文件有相应的硬链接,那么这个文件依然会保留,直到所有对它的引用都被删除。

使用与注意事项

ln 源文件 目标文件 : 创建硬链接文件

比如 ln test2.sh test_l.sh。这里test_l.sh就是链接到了test2.sh脚本文件

ln -s test2.sh test_s.sh 这样做就是软链接。

注意事项:

1.修改源文件或者目标文件,对应另外一个文件也会发生相应修改

2.删除源文件或者目标文件,对另外一个文件没有影响

3.硬链接文件不占用存储空间

4.不能对目录文件进行创建硬链接操作

5.硬链接文件不能跨文件系统

远程上传本地文件

scp test1 CLC@192.168.27.227:/home/CLC

image-20230728205149193

file test:查看文件属性,文件属于在ARM架构上运行的程序,可以在树莓派上运行。

image-20230728210800662

在电脑上的虚拟机上编译文件属性为x86-64位运行的程序

echo $PATH 获得当前环境变量的值

2.1 临时有效,配置环境变量
	PATH 环境变量
	export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/CLC/lessonPI/tools-master/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin
2.2 永久有效,配置环境变量

	修改工作目录下的.bashrc 隐藏文件,配置命令终端的
	 vi /home/CLC/.bashrc 
	 在文件最后一行加入:
	 	export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/CLC/lessonPI/tools-master/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin
	 source /home/CLC/.bashrc 加载配置文件,马上生效配置。

linux内核目录结构介绍:

(219条消息) Linux内核目录结构介绍(超详细)_kernel内核在哪里_ZK悟空的博客-CSDN博客

image-20230728222553192

linux内核结构框图

Linux内核0.11——内核体系结构_SwhiteDev的博客-CSDN博客

image-20230731011154720

当将Linux驱动编译进内核和编译成模块时,有以下区别:

  1. 编译驱动进内核: 当将驱动直接编译进内核时,驱动代码将成为内核镜像的一部分。这意味着在编译过程中,驱动代码会与内核代码合并,最终生成的内核镜像将包含驱动的所有功能。

    优点:

    • 由于驱动是内核的一部分,它会在启动时加载,确保驱动一直可用且随时可以使用。
    • 稍微更好的性能:因为驱动代码与内核内嵌在一起,不存在加载/卸载模块的开销。

    缺点:

    • 内核镜像大小较大:每个编译进内核的驱动都会增加内核的大小,导致内存占用较大。
    • 灵活性差:不能动态加载或卸载驱动,它始终存在于内核中。这可能会在想要节省内存或处理不同硬件配置时造成问题。
  2. 编译驱动成为模块: 当将驱动编译为可加载的内核模块时,它以单独的文件形式存在于文件系统中(例如,.ko文件)。模块可以根据需要动态地加载和卸载到内核中。

    优点:

    • 内核镜像大小较小:由于驱动是单独的模块,它不会增加内核镜像的大小,使内核保持紧凑。
    • 灵活性:可以在运行时动态加载或卸载驱动模块,这对于资源有限的系统或需要支持不同硬件配置的情况非常有用,而无需重新编译整个内核。
    • 更容易更新:可以在不重新构建整个内核的情况下更新或更改驱动模块。

    缺点:

    • 略微降低性能:加载和卸载模块可能会引入一些开销,尽管通常可以忽略不计。
    • 依赖性:有时,模块可能会依赖其他模块或内核配置,使得管理略微复杂。

总体而言,建议尽可能将驱动编译为模块,因为它提供了更大的灵活性并减少了内核的大小。但是,对于系统必需的关键驱动程序(例如存储或文件系统驱动程序),有时将它们编译进内核中可能更有益,以确保它们始终可用且不会被意外卸载。

Linux驱动编译成模块:

交叉编译环境配置:

2.1 临时有效,配置环境变量
		PATH 环境变量
		export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/CLC/lessonPI/tools-master/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin
	2.2 永久有效,配置环境变量

		修改工作目录下的.bashrc 隐藏文件,配置命令终端的
		 vi /home/CLC/.bashrc 
		 在文件最后一行加入:
		 	export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/CLC/lessonPI/tools-master/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin
		 	/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/xiaohu/System/tools-master/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin
		 source /home/CLC/.bashrc 加载配置文件,马上生效配置。

首先需要配置并编译内核:

4.驱动代码的编写
				驱动代码的编译需要一个提前编译好的内核
					编译内核就必须配置
			配置的最终目标会生成 .config文件,该文件指导Makefile去把有用东西组织成内核

	
				厂家配linux内核源码,比如说买了树莓派,树莓派linux内核源码
			第一种方式:
				cp 厂家.config .config
			第二种方式:
				make menuconfig 一项项配置,通常是基于厂家的config来配置
			第三种方式:
				完全自己来


			如何配置树莓派的Linux内核
			find . -name *_defconfig
			驱动两种加载方式:
				* 编译进内核  zImage包含了驱动
				M 模块方式生成驱动文件xxx.ko  系统启动后,通过命令inmosd xxx.ko 加载


			内核配置:
			ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make bcm2709_defconfig
			指定ARM架构   指定编译器                      树莓派          主要核心指令
5. 树莓派Linux内核编译
			
			5.1 编译:
			ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 zImage modules dtbs
																			指定用多少电脑资源进行编译
																			zImage生成内核镜像
																			modules要生成驱动模块
																			dtbs生成配置文件

			5.2 编译成功后,看到源码树目录多了vmlinux,失败则无此文件
			成功后,目标zImage镜像arch/arm/boot底下

			5.3 打包zImage成树莓派可用的xxx.img
			./scripts/mkknlimg arch/arm/boot/zImage ./kernel_new.img

			5.4 数据拷贝
				mkdir data1 data2
				挂载U盘
				sudo mount /dev/sdb1 data1   一个fat分区,是boot相关的内容,kernel的img
				sudo mount /dev/sdb2 data2   一个是ext4分区,也就是系统的根目录分区。

				安装modules, 设备驱动文件: hdmi usb wifi io ...
				sudo ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make INSTALL_MOD_PATH=/home/chenlc/data2 modules_install

				安装更新 kernel.img 文件,注意镜像名字是kernel7.img
				先备份
					cd /home/chenlc/data1
					cp kernel7.img kernel7OLD.img
				再把编译新生成的拷贝到data1,起名kernel7.img
					cp kernel_new.img /home/chenlc/data1/kernel7.img


				拷贝配置文件
				cp arch/arm/boot/dts/.*dtb* /home/chenlc/data1

厂家配linux内核源码,比如说买了树莓派,树莓派linux内核源码
第一种方式:
cp 厂家.config .config

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make bcm2709_defconfig根据厂家给的bcm2709config,生成了bcm2709_defconfig

编译:

	ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 zImage modules dtbs
																			指定用多少电脑资源进行编译
																			zImage生成内核镜像
																			modules要生成驱动模块
																			dtbs生成配置文件

image-20230731223137404

生成vmlinux,在arch/arm/boot下生成zImage表示内核编译成功。

成功后,打包目标镜像zImage成树莓派可以使用的xxx.img镜像

./scripts/mkknlimg arch/arm/boot/zImage ./kernel_new.img

mkdir data1 data2
				挂载U盘
				sudo mount /dev/sdb1 data1   一个fat分区,是boot相关的内容,kernel的img
				sudo mount /dev/sdb2 data2   一个是ext4分区,也就是系统的根目录分区。

				安装modules, 设备驱动文件: hdmi usb wifi io ...
				sudo ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make INSTALL_MOD_PATH=/home/xiaohu/data2 modules_install

				安装更新 kernel.img 文件,注意镜像名字是kernel7.img
				先备份
					cd /home/xiaohu/data1
					cp kernel7.img kernel7OLD.img
				再把编译新生成的拷贝到data1,起名kernel7.img
					cp kernel_new.img /home/chenlc/data1/kernel7.img


				拷贝配置文件
				1.cp arch/arm/boot/dts/*.dtb /home/xiaohu/data1
				2.cp arch/arm/boot/dts/overlays/.*dtb* /home/xiaohu/data1/overlays/	
				3.cp arch/arm/boot/dts/overlays/README /home/xiaohu/data1/overlays/

image-20230731230022599

md5的值一样,拷贝成功。

image-20230731231315101

我们可以看到内核已经更新了

​ 第二种方式:
​ make menuconfig 一项项配置,通常是基于厂家的config来配置

image-20230731213300957

  • 编译进内核 zImage包含了驱动
    M 模块方式生成驱动文件xxx.ko 系统启动后,通过命令inmosd xxx.ko 加载

第三种方式:
完全自己来

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
将生成的.ko文件scp到运行的树莓派上;
使用insmod命令加载模块驱动;
更改添加的模块权限:chmod 666 pin4
运行测试代码,在dmesg查看内核打印的信息
发现驱动模块加载成功。
不使用后可以使用rmmod来卸载模块驱动

image-20230801001713198

image-20230801001935670

linux驱动开发

博通BCM2835

image-20230802192821834

GPFSEL0 = 0x3f200000

GPFSEL0 GPIO Function Select 0 功能选择 输入/输出 (gpio function select registers)32位功能选择寄存器

GPSET0 GPIO Pin Output Set 0 输出1

0 = NO effect
1 = Set GPIO Pin n

GPCLR0 GPIO Pin Output Clear 0 清0

0 = No effect
1 = Clear GPIO pin n

image-20230802200153755

BCM2709的物理地址应该是从0x3f200000开始的,在这个基础上进行linux的MMU内存虚拟化管理,映射到虚拟地址上。

image-20230802200421287

image-20230802202327085

设置pin4为输出引脚,就要把14-12寄存器写入001

所以要将 14-13位变为0,12位为1,

0000000000000000000 110 0000000000000000取反得111111111111111111111 001 111111111111111

110 左移12位得上面的数字,取反&原来的寄存器初始值,即可把14-13位置0,

*GPFSEL0 &= ~(0x6 <<12) //把bit13和bit14值为0

1左移12位 |上原来的寄存器初始值,即可把12位置1

*GPFSEL0 |= (0x1 <<12) //把bit12值为1

驱动代码:

//pin4driver.c:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/types.h>
#include <asm/io.h>

static struct class *pin4_class;
static struct device *pin4_class_dev;

static dev_t devno;  					//设备号
static int major =231; 					//主设备号
static int minor  = 0; 					//次设备号
static char *module_name = "pin4"; 		//模块名

volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0  = NULL; //置1
volatile unsigned int* GPCLR0  = NULL; //清0

static int pin4_open(struct inode *inode, struct file *file) 
{
	//配置pin4引脚为输出引脚 pin
	*GPFSEL0 &= ~(0x6 <<12); //把bit13和bit14值为0
	*GPFSEL0 |=  (0x1 <<12);  //把bit12值为1
	printk("pin4_open\n");

	return 0;
}

static ssize_t pin4_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
	int userCmd;
	//获取上层write的值
	copy_from_user(&userCmd,buf,count);
	//根据值来操作io口,高低电平的写入
	printk("pin4 write\n");
	if(userCmd == 1){
		*GPSET0 |= 0x1 << 4;
	}else if(userCmd == 0){
		*GPCLR0 |= 0x1 << 4;
	}else{
		printk("undo\n");
	}
	

	return 0;
}

static ssize_t pin4_read (struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	printk("pin4_read\n");

	return 0;
}


static struct file_operations pin4_fops ={
	.owner = THIS_MODULE,
	.open = pin4_open,
	.write = pin4_write,
	.read = pin4_read,
};

int __init pin4_drv_init(void) //真实的驱动入口
{
	int ret;
	printk("insmod driver pin4 success\n");
	devno = MKDEV(major,minor); //2.创建设备号
	ret = register_chrdev(major, module_name, &pin4_fops); //3.注册驱动 告诉内核把这个驱动加入到内核的链表内

	pin4_class = class_create(THIS_MODULE,"myfirstdeno"); //让代码在dev自动生成设备
	pin4_class_dev = device_create(pin4_class,NULL,devno,NULL,module_name); //创建设备文件

	
	GPFSEL0 = (volatile unsigned int*)ioremap(0x3f200000,4); //ioremap 将物理地址转换为虚拟地址,io口寄存器映射成普通内存单元进行访问
	GPSET0  = (volatile unsigned int*)ioremap(0x3f20001c,4);
	GPCLR0  = (volatile unsigned int*)ioremap(0x3f200028,4);
	
	return 0;
}

void __exit pin4_drv_exit(void)
{
	iounmap(GPCLR0);
	iounmap(GPSET0);  
	iounmap(GPFSEL0);//iounmap 将物理地址与虚拟地址解绑
	
	device_destroy(pin4_class,devno);
	class_destroy(pin4_class);
	unregister_chrdev(major,module_name); //卸载驱动

}

module_init(pin4_drv_init); //入口,内核加载驱动时,这个宏会被调用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");



	

测试代码:

//pin4test.c:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/types.h>
#include <asm/io.h>

static struct class *pin4_class;
static struct device *pin4_class_dev;

static dev_t devno;  					//设备号
static int major =231; 					//主设备号
static int minor  = 0; 					//次设备号
static char *module_name = "pin4"; 		//模块名

volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0  = NULL; //置1
volatile unsigned int* GPCLR0  = NULL; //清0

static int pin4_open(struct inode *inode, struct file *file) 
{
	//配置pin4引脚为输出引脚 pin
	*GPFSEL0 &= ~(0x6 <<12); //把bit13和bit14值为0
	*GPFSEL0 |=  (0x1 <<12);  //把bit12值为1
	printk("pin4_open\n");

	return 0;
}

static ssize_t pin4_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
	int userCmd;
	//获取上层write的值
	copy_from_user(&userCmd,buf,count);
	//根据值来操作io口,高低电平的写入
	printk("pin4 write\n");
	if(userCmd == 1){
		*GPSET0 |= 0x1 << 4;
	}else if(userCmd == 0){
		*GPCLR0 |= 0x1 << 4;
	}else{
		printk("undo\n");
	}
	

	return 0;
}

static ssize_t pin4_read (struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	printk("pin4_read\n");

	return 0;
}


static struct file_operations pin4_fops ={
	.owner = THIS_MODULE,
	.open = pin4_open,
	.write = pin4_write,
	.read = pin4_read,
};

int __init pin4_drv_init(void) //真实的驱动入口
{
	int ret;
	printk("insmod driver pin4 success\n");
	devno = MKDEV(major,minor); //2.创建设备号
	ret = register_chrdev(major, module_name, &pin4_fops); //3.注册驱动 告诉内核把这个驱动加入到内核的链表内

	pin4_class = class_create(THIS_MODULE,"myfirstdeno"); //让代码在dev自动生成设备
	pin4_class_dev = device_create(pin4_class,NULL,devno,NULL,module_name); //创建设备文件

	
	GPFSEL0 = (volatile unsigned int*)ioremap(0x3f200000,4); //ioremap 将物理地址转换为虚拟地址,io口寄存器映射成普通内存单元进行访问
	GPSET0  = (volatile unsigned int*)ioremap(0x3f20001c,4);
	GPCLR0  = (volatile unsigned int*)ioremap(0x3f200028,4);
	
	return 0;
}

void __exit pin4_drv_exit(void)
{
	iounmap(GPCLR0);
	iounmap(GPSET0);  
	iounmap(GPFSEL0);//iounmap 将物理地址与虚拟地址解绑
	
	device_destroy(pin4_class,devno);
	class_destroy(pin4_class);
	unregister_chrdev(major,module_name); //卸载驱动

}

module_init(pin4_drv_init); //入口,内核加载驱动时,这个宏会被调用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

运行结果:

image-20230802214746012

image-20230802214758933

posted @ 2023-08-04 19:40  吃猪肉的猪  阅读(106)  评论(0)    收藏  举报