树莓派基本开发

15 篇文章 2 订阅
10 篇文章 1 订阅

一、树莓派基本配置

1、sd卡烧录

需要的设备、软件和文件:

SD卡(不小于8g)
读卡器
Win32DiskImager2.0.1.8.exe (烧录软件)
树莓派系统镜像

在这里插入图片描述

先选择烧录进哪个SD卡,然后打开需要烧录的系统镜像,最后点击写入。接下来交给时间……
在这里插入图片描述

在这里插入图片描述

2、开机自动连接wifi

我们希望树莓派开机自动连接wifi,打开树莓派系统的boot磁盘,新建名为叫“wpa_supplicant.txt”的文件写入下列内容并保存。最后把 .txt 的后缀变成 .conf 的后缀

country=CN
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
    ssid="FAST_502"		# wifi 名字
    psk="ZHL502502"		# wifi密码
    priority=3			# 优先级,数字越大优先级越大
}
network={
    ssid="Zdd-4089158"
    psk="1234567890"
    priority=2
}
network={
    ssid="Zdd-4089158"
    psk="1234567890"
    priority=1
}

3、树莓派开机登录方式(仅介绍三种)

串口登录模式

1.打开树莓派的根目录下的"config.txt"文件,在文末的位置添加
dtoverlay=pi3-miniuart-bt

2.修改根目录的"cmdline.txt"的内容
	先在原本的内容前面添加 # 进行注释,然后将添加下列内容:
dwc_otg.lpm_enable=0 console=tty1 console=serial0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

树莓派默认账号和密码

账号:pi
密码:raspberry

利用USB-TTL模块连接树莓派和PC端(VCC可以不接,但是GND必须要接)

树莓派TTL
TXD----->RXD
RXD----->TXD
GND----->GND

打开MobaXterm软件
在这里插入图片描述

上电树莓派出现系统加载说明串口登录成功,最后登录账号密码即可
在这里插入图片描述
在这里插入图片描述

SSH登录模式

ssh连接的前提是pc端和树莓派需要在同一局域网中,就是pc端和树莓派需要连接同一个wifi

在树莓派的根目录下创建 ssh 文件,然后保存
前面我们已经可以开机自动连接了wifi ,然后我们通过串口来获取树莓派的ip地址。

pi@raspberrypi:~$ ifconfig
eth0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        ether b8:27:eb:58:f2:c1  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

wlan0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.106  netmask 255.255.255.0  broadcast 255.255.255.255
        inet6 fe80::13de:c07b:b5b6:2eb1  prefixlen 64  scopeid 0x20<link>
        ether b8:27:eb:0d:a7:94  txqueuelen 1000  (Ethernet)
        RX packets 28  bytes 3798 (3.7 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 39  bytes 5853 (5.7 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

由此我们可以知道树莓派的ip地址是192.168.1.106

打开MobaXterm软件,按照要求输入ip地址,用户名,端口(默认22)密码
在这里插入图片描述
在这里插入图片描述

xrdp登录模式

在树莓派的命令终端中输入

sudo sudo apt-get update
sudo apt-get install xrdp

打开PC端的远程桌面连接,输入ip地址,用户名,密码即可
在这里插入图片描述

4、换源

树莓派登录系统之后做的第一件是应该是换源,后面树莓派需要下载软件等文件,不换源速度会很慢。不要问为什么换,不换可不可以?不换源就可能安装不了相对于的软件,速度会很慢。一句话,你喜欢就行!!!!
还有一件事,换源不是随随便便换个源就可以了的,需要对应的版本号。比如系统是A型号你换源的是B型号,这肯定不行。

Ctrl + Alt + t 打开命令终端
输入命令:

pi@raspberrypi:~ $ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 10 (buster)
Release:        10
Codename:       buster

由此我们可以知道系统的版本代号是 buster

sudo nano /etc/apt/sources.list

将原本的内容前面加个 # 进行注释,并用添加下列内容

#(由于系统的不同所以版本代号也会有所不用,如果系统版本代号是stretch,那么用stretch全部替换buster )
deb http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ buster main contrib non-free rpi
deb-src http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ buster main contrib non-free rpi
sudo nano /etc/apt/sources.list.d/raspi.list

将原本的内容注释并添加下列清华源

deb http://mirror.tuna.tsinghua.edu.cn/raspberrypi/ buster main ui
deb-src http://mirror.tuna.tsinghua.edu.cn/raspberrypi/ buster main ui

最后升级一下,如果没有提示警告或者错误就说明换源成功

sudo apt-get update
sudo apt-get install aptitude -y
sudo aptitude update && sudo aptitude upgrade -y 

aptitude 的基本用法

命令描述
aptitude update更新可用的包列表
aptitude upgrade升级可用的包
aptitude dist-upgrade将系统升级到新的发行版
aptitude install pkgname添加安装包
aptitude remove pkgname删除包
aptitude purge pkgname删除包及其配置文件
aptitude search string搜索包
aptitude show pkgname显示包的详细信息
aptitude clean删除下载的包文件
aptitude autoclean仅删除过期的包文件

5、安装python

python的官网进行下载源码进行编译安装
在这里插入图片描述
以python3.9.9为例,下载源码。
在这里插入图片描述
pc端下载后传输至树莓派,然后进行解压和做一些编译python之前的一些准备

# 解压命令
tar -zxvf Python-3.9.9.tgz
# 安装python依赖
sudo aptitude install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev wget -y
sudo apt-get install -y make build-essential libssl-dev zlib1g-dev   
sudo apt-get install -y libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm 
sudo apt-get install -y libncurses5-dev  libncursesw5-dev xz-utils tk-dev

来到已解压的文件夹,然后编译,安装python

cd Python-3.9.9/
sudo ./configure && sudo make -j4 && sudo make install

经过漫长的等待……终于安装好了python

输入命令,检查python版本是否正确
python3.9 -V

可以创建软链接来快速启动python3.9

sudo ln -s /usr/local/bin/python3.9 /usr/bin/python
#如果python已存在,则先删除在创建软连接
sudo rm /usr/bin/python

6、安装pip pip3

# 获取get-pip.py
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3.9 get-pip.py --no-warn-script-location    # 完成安装

pip 换源

  1. 永久的换源
    pip下载python库的时候也是会很慢,所以我们需要进行换源
mkdir ~/.pip
sudo nano ~/.pip/pip.conf
写入下列内容并保存

[global]
timeout = 10
index-url =  http://mirrors.aliyun.com/pypi/simple/
extra-index-url= http://pypi.douban.com/simple/
[install]
trusted-host=
    mirrors.aliyun.com
    pypi.douban.com
  1. 暂时的换源下载
pip install 库名 -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com

7、安装opencv

pip install numpy
pip install matplotlib
pip install opencv-python

8、HDMI无法显示

将SD卡拔出,插入读卡器,再插电脑,修改
/boot/config.txt    
将 #hdmi_force_hotplug=1
前面的“#”去掉,再将SD卡插回树莓派,开机即可

二、linux静态库和动态库编译

在linux中静态库文件为 .a 文件,动态库文件为 .so 文件(与win10的lib静态库dll动态库类似)

linux中命名系统中共享库的规则

libname.so.x.y.z
1. lib:  固定代表共享库
2. name: 共享库名称
3. .so:  固定后缀
4. x:    主版本号
5. y:    次版本号
6. z:    发行版本号

静态函数库,程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库,是在程序执行前就加入到目标程序中去了

动态函数库,程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。是在程序执行时动态(临时)由目标程序去调用

静态库动态库
优点运行快,发布程序时无需提供静态库,移植方便1.链接时不复制,程序运行时由系统动态加载到内存,供程序使用,系统只加载一次,多个程序可以共用,节省内存 2. 程序升级简单
缺点1. 链接时完整地拷贝至可执行文件中,被多次使用就有多分冗余拷贝 2. 更新、部署、发布麻烦1. 加载速度比静态库慢 2. 发布程序需要提供依赖的动态库

静态库的制作

单文件静态库制作

// add.c
#include "add.h"
int add(int a, int b)
{
    printf("*************add**************\n");

    return (a + b);
}
// add.h
#ifndef __ADD_H
#define __ADD_H

#include <stdio.h>
int add(int a, int b);

#endif
// main.c
#include "add.h"

int main(void)
{
    int a, b;
    scanf("%d", &a);
    scanf("%d", &b);

    printf("%d + %d = %d \n", a, b, add(a, b));

    return 0;
}

编译过程:

1. 三个文件放在同一路径下
2. gcc -c add.c  					// 生成.o 文件
3. ar rcs libadd.a add.o			// 生成静态库 .a文件
4. gcc main.c libadd.a -o main		// 生成可执行文件

多文件静态库制作

// add.c
#include "add.h"
int add(int a, int b)
{
    printf("*************add**************\n");

    return (a + b);
}
// add.h
#ifndef __ADD_H
#define __ADD_H

#include <stdio.h>
int add(int a, int b);

#endif
// mul.c
#include "mul.h"

int mul(int a, int b)
{
    printf("*************mul**************\n");

    return (a * b);
}
// mul.h
#ifndef __MUL_H
#define __MUL_H

#include <stdio.h>
int mul(int a, int b);

#endif

编译过程:

gcc -c *.c						// 生成 .o 文件
ar rc libnum.a *.o				// 生成静态库 .a 文件
gcc main.c libnum.a	-o main		// 生成可执行文件

编译补充:

gcc main.c -lnum -L ./ -I ./ -o main

-lnum :意思是添加静态库 libnum.a 的文件
-I :在指定地址寻找头文件
-L :在指定地址寻找静态库

在这里插入图片描述
文件树如图所示,如何编译main.c 文件呢?

gcc main.c Static/linnum.a -I include/
编译main.c 时通过-I 指定头文件路径

动态库的制作

单文件制作动态库

main.c add.c add.h在同一路径下

gcc -shared -fpic add.c -o add.so		// 生成动态库文件
gcc main.c add.so -Wl,-rpath=./			// 编译指定路径下的动态库文件,生成可执行文件

多文件制作动态库
在这里插入图片描述

在malloc文件夹中生成动态库
// 编译user文件夹下的.c文件
gcc -shared -fpic ../user/*.c -o num.so -I ../include/  // 生成num.so 文件

在main.c的路径下编译生成可执行文件
gcc main.c ./malloc/num.so -Wl,-rpath=./ -I ./include/
-Wl,-rpath=./  表示程序执行时优先寻找指定路径
-I 指定头文件路径

补充:

静态库生成的可执行文件可以随便移动,但是动态库的的可执行文件不能单独移动。
动态库可执行文件需要和动态库等文件一起移植,所以静态库的移植性好于动态库。

三、树莓派外设库的使用(wiringPi)

wiringPi库是什么?

wiringPi库是树莓派控制IO口的驱动程序,让我们直接调用里面的API进而快速的开发编程。我们只需要学会如何去使用wiringPi库,后面可以按照需求或者学习的需要自己编写驱动库。

判断自己的树莓派是否有wiringPi库

gpio -v

在这里插入图片描述
未安装可以根据wiringPi库安装链接进行安装

1、wiringPi基本函数

头文件 #include “wiringPi.h”

硬件初始化函数

函数说明参数返回值
int wiringPiSetup (void)使用wiringPi引脚编号表,编号0~16。例如:wiringPi的P0引脚编号是0,BCM编号是17-1表示初始化失败
int wiringPiSetupGpio (void)使用BCM编号-1表示初始化失败

通用GPIO控制函数

函数说明参数返回值
void pinMode (int pin, int mode)只有wiringPi 引脚编号下的1脚(BCM下的18脚) 支持PWM输出;只有wiringPi编号下的7(BCM下的4号)支持GPIO_CLOCK输出。其他IO口都可以输入输出mode:INPUT、OUTPUT、PWM_OUTPUT,GPIO_CLOCK
void digitalWrite (int pin, int value)输出高低电平HIGH,LOW
int digitalRead (int pin)返回读取到引脚的电平pin:读取的引脚号1或0
void analogWrite(int pin, int value)树莓派的引脚本身是不支持AD转换的,需要增加另外的模块配合使用value:输出的模拟量
int analogRead (int pin)树莓派的引脚本身是不支持AD转换的,需要增加另外的模块配合使用pin:引脚号返回引脚上读取的模拟量
void pwmWrite (int pin, int value)输出pwm,pin只能是wiringPi 引脚编号下的1脚(BCM下的18脚)value:写入到PWM寄存器的值,范围在0~1024之间。
如何编译带wiringPi库的C文件?

gcc main.c -lwiringPi

2、wiringPi库实现输出IO口的高低电平

#include <wiringPi.h>
#include <stdio.h>

#define pin 7

int main()
{
    int cmd;
    if (wiringPiSetup() == -1)
    {
        printf("init failt!!!\n");
        return -1;
    }
    pinMode(pin, OUTPUT);    // 输出模式
    digitalWrite(pin, HIGH); // pin引脚输出高电平

    while (1)
    {
        printf("input 0 or 1:\n");
        scanf("%d", &cmd);
        // getchar();
        if (cmd == 0)
        {
            printf("output low\n\n");
            digitalWrite(pin, LOW); // pin引脚输出低电平
        }
        else if (cmd == 1)
        {
            printf("output high\n\n");
            digitalWrite(pin, HIGH); // pin引脚输出高电平
        }
        else
        {
            printf("input error\n\n");
        }
    }
    return 0;
}

3、wiringPi库实现超声波测距

Linux下的时间函数:struct timeval结构体

#include "sys/time.h"
 
struct timeval  
{  
__time_t tv_sec;        /* Seconds. */  
__suseconds_t tv_usec;  /* Microseconds. */  
}; 

用法

  1. 定义两个结构体变量 
  	 struct timeval t1;
  	 struct timeval t2;
  
  2. 获取事件的开始、结束信息 
     gettimeofday(&t1, NULL);
	 //....事件
	 gettimeofday(&t2, NULL);

超声波原理

保证Trig引脚信号一开始先置低电平,然后置高电平10us,10us之后置低电平
检测Echo为高电平的持续时间,通过公式:dis = time / 1000000 * 34000 / 2 (单位:cm)
time是Echo为高电平的时间
#include <wiringPi.h>
#include <stdio.h>
#include <sys/time.h>

#define Trig 2
#define Echo 3

void ultraInit(void)
{
    pinMode(Echo, INPUT);  // 输入模式
    pinMode(Trig, OUTPUT); // 输出模式
}

float disMeasure(void)
{
    struct timeval tv1;
    struct timeval tv2;

    /*
        struct timeval
        {
            time_t tv_sec;        // 秒.
            suseconds_t tv_usec;  // 微秒.
        };
    */

    long start, stop;
    float dis;

    digitalWrite(Trig, LOW);    // 保证一开始为低电平
    delayMicroseconds(2);

    digitalWrite(Trig, HIGH);   // 高电平10us
    delayMicroseconds(10);
    digitalWrite(Trig, LOW);    // 10us之后拉低

    //统计高电平持续的时间
    while (!(digitalRead(Echo) == 1));
    gettimeofday(&tv1, NULL);   // 开始的时间
    while (!(digitalRead(Echo) == 0));  
    gettimeofday(&tv2, NULL);   // 结束的时间
    
    start = tv1.tv_sec * 1000000 + tv1.tv_usec; //单位为us
    stop = tv2.tv_sec * 1000000 + tv2.tv_usec;

    dis = (float)(stop - start) / 1000000 * 34000 / 2; // s×cm/s=cm

    return dis;
}

int main(void)
{
    float dis;

    if (wiringPiSetup() == -1)
    {
        printf("setup wiringPi failed !");
        return 1;
    }

    ultraInit();

    while (1)
    {
        dis = disMeasure();
        printf("distance = %0.2f cm\n", dis);
        delay(1000);
    }

    return 0;
}

四、wiringPi库实现串口通信

全双工:同一时间收发双方可以同时传输数据
半双工:同一时间收发只能一方在传输数据
单工:只能接收或者发送数据

串口通信注意:

1. 数据格式
	数据位、停止位、奇偶校验位
	
2. 波特率
	波特率是1s传输了多少个码元,单位bps

API函数

函数说明参数返回值
int serialOpen (char *device, int baud)打开并初始化串口的驱动device:串口的地址,即设备所在的目录。默认"/dev/ttyAMA0" baud:波特率正常返回文件描述符,否则返回-1失败。
void serialClose (int fd)关闭fd关联的串口fd:文件描述符
void serialPutchar (int fd, unsigned char c)发送一个字节c:发送的字符
void serialPuts (int fd, char *s)发送一个字符串s:字符串
int serialGetchar (int fd)从串口读取一个字节数据返回。如果串口缓存中没有可用的数据,则会等待10秒,如果10后还没有,返回-1。所以在读取前,通过serialDataAvail判断。fd:文件描述符返回读取到的字符
int serialDataAvail(int fd)获取串口缓存中可用的字节数fd:文件描述符可读取的字节数,-1代表错误

初次使用串口需要做些简单的配置

1. cd /boot/
2. sudo nano cmdline.txt
    将原本的内容做好备份,然后将下列的内容进行替换
   console=tty1 root=PARTUUID=ea7d04d6-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet splash plymouth.ignore-serial-consoles
3. 树莓派重启
   sudo reboot 

TTL模块与树莓派的连接(需要共地,保证数据正常)

树莓派TTL
TXD----->RXD
RXD----->TXD
GND----->GND

发送一个字符串

#include <wiringPi.h>
#include <wiringSerial.h>
#include <stdio.h>

int main()
{
    int fd;
    if (-1 == wiringPiSetup())
    {
        printf("bsp init failt\n");         // 初始化失败
    }       
    fd = serialOpen("/dev/ttyAMA0", 9600);  // 打开串口,波特率9600

    while (1)
    {
        // serialPutchar(fd,'c');  // 发送一个字符
        serialPuts(fd, "Raspberry!!\r\n"); // 发送一个字符串
        delay(1000);
    }
    return 0;
}

接收一个字符

#include <wiringPi.h>
#include <wiringSerial.h>
#include <stdio.h>

int main()
{
    int fd;
    char cmd;

    if (-1 == wiringPiSetup())
    {
        printf("bsp init failt\n"); // 初始化失败
    }
    fd = serialOpen("/dev/ttyAMA0", 9600); // 打开串口,波特率9600

    while (1)
    {
        while (serialDataAvail(fd) != -1)
        {
            //当缓冲区有数据时
            cmd = serialGetchar(fd);
            printf("cmd = %c\n", cmd);
        }
        return 0;
    }
    serialClose(fd);
}

在linux中一切皆文件,设备虽然是硬件但在linux中还是以文件的形式存在。连接linux与硬件的桥梁是底层驱动,我们可以理解wiringPi是一个底层驱动。

查找文件是否存在

find -name relay.c

find ./-name relay.c   #在当前路径下寻找

五、树莓派的交叉编译

概念

交叉编译是什么?交叉编译是在一个平台上生成另一个平台上的可执行代码,例如在pc端上通过keil5编程,最后得到可以在51、stm32上运行的hex文件。这种方式就叫交叉编译

为什么要交叉编译?因为在目标平台上不允许或不能安装我们需要的编程环境,目标平台一开始设计的目的就不在于是否可以编程,只需要能运行该平台上的可执行文件即可,所以我们需要在主机上进行安装好编程环境,然后通过编译生成目标平台上可执行的文件。

C51通过win10上的kei5进行编程,得到可执行文件。
最后可执行代码是在C51上运行,win10只是起编程、编译作用

树莓派的ubuntu是ARM—Linxu平台,在虚拟机上的ubuntu生成的可执行代码是x86平台
所以虚拟机上的ubuntu的可执行代码不能在树莓派上运行

工具

  • 交叉编译器
  • 交叉编译工具链

树莓派交叉编译工具下载

在这里插入图片描述
下载完成之后拷贝到Linux虚拟机中

cd tools  // 进入交叉编译工具包

在这里插入图片描述

cd arm-bcm2708/
cd gcc-linaro-arm-linux-gnueabihf-raspbian-x64/
cd bin/

arm-linux-gnueabihf-gcc   ---> 就是树莓派交叉编译工具链

在这里插入图片描述
此时arm-linux-gnueabihf-gcc是一个软链接,它指向的是arm-linux-gnueabihf-gcc-4.8.3可执行文件。
软链接不占内存只是一个符号指向了这个位置,类似于windowns的快捷方式。

添加环境变量

因为有的可执行文件在很多路径之下才能找到,如果我们不来到这个路径下是找不到该文件的。所以我们需要设置好环境变量,让系统自动帮我们到指定的路径下寻找可执行文件。这就是环境变量的用处

查看环境变量
echo $PATH

环境变量永久有效设置

sudo nano ~/.bashrc
在最后的结尾添加:
export PATH=$PATH:路径			# 添加绝对路径
保存退出,输入命令:
source .bashrc 					# 使能有效环境变量
最后查看一下是否添加成功
echo $PATH

gcc 和 arm-linux-gnueabihf-gcc的区别

虚拟机上的gcc是编译出来的可执行程序是x86平台的
arm-linux-gnueabihf-gcc编译的可执行文件是arm平台的

在这里插入图片描述
编译了之后通过scp命令传输至树莓派

scp file user@ip:path

file :是需要传输的目标文件
user :是目标主机的用户名
ip   :是目标主机的网络ip地址
path :是将文件传输目标主机的哪个路径

scp main pi@192.168.1.106:/home/pi      # 一般是需要root

软、硬链接

  • 软链接
    又称之为:符号连接(Symbolic Link)。类似于Windows的快捷方式。在选定的位置上生成一个文件的镜像,不会占用磁盘空间,包含有位置信息。

  • 硬链接
    选定的位置上生成一个和源文件大小相同的文件,作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。

简单进行交叉编译一下

// main.c

#include "stdio.h"

int main(void)
{
	printf("Welcom to Linux!!!\n");
	return 0;
}
# 交叉编译
arm-linux-gnueabihf-gcc main.c -o main

# 移植到树莓派
scp main pi@192.168.1.106:/homw/pi

带wiringPi库的交叉编译

关于带wiringPi库的编译跟前面动态库的编译是一样的,首先我们需要获取wiringPi库的动态库文件

https://download.csdn.net/download/weixin_54252044/86400522

解压后拷贝到虚拟机ubuntu中的,目录如下
在这里插入图片描述

实现超声波代码带wiringPi库的交叉编译

// 超声波
#include <wiringPi.h>
#include <stdio.h>
#include <sys/time.h>

#define Trig 2
#define Echo 3

void ultraInit(void)
{
    pinMode(Echo, INPUT);  // 输入模式
    pinMode(Trig, OUTPUT); // 输出模式
}

float disMeasure(void)
{
    struct timeval tv1;
    struct timeval tv2;

    /*
        struct timeval
        {
            time_t tv_sec;        // 秒.
            suseconds_t tv_usec;  // 微秒.
        };
    */

    long start, stop;
    float dis;

    digitalWrite(Trig, LOW);    // 保证一开始为低电平
    delayMicroseconds(2);

    digitalWrite(Trig, HIGH);   // 高电平10us
    delayMicroseconds(10);
    digitalWrite(Trig, LOW);    // 10us之后拉低

    //统计高电平持续的时间
    while (!(digitalRead(Echo) == 1));
    gettimeofday(&tv1, NULL);   // 开始的时间
    while (!(digitalRead(Echo) == 0));  
    gettimeofday(&tv2, NULL);   // 结束的时间
    
    start = tv1.tv_sec * 1000000 + tv1.tv_usec; //单位为us
    stop = tv2.tv_sec * 1000000 + tv2.tv_usec;

    dis = (float)(stop - start) / 1000000 * 34000 / 2; // s×cm/s=cm

    return dis;
}

int main(void)
{
    float dis;

    if (wiringPiSetup() == -1)
    {
        printf("setup wiringPi failed !");
        return 1;
    }

    ultraInit();

    while (1)
    {
        dis = disMeasure();
        printf("distance = %0.2f cm\n", dis);
        delay(1000);
    }

    return 0;
}
两种wiringPi交叉编译方式

arm-linux-gnueabihf-gcc chao.c -I ~/WiringPi/wiringPi /usr/lib/libwiringPi.so -o chao
或
arm-linux-gnueabihf-gcc chao.c -I ~/WiringPi/wiringPi -lwiringPi -L /usr/lib -o chao

最后将可执行文件移植到树莓派运行,perfect 完美运行

六、ubuntu编译树莓派linux内核

为什么要编译内核?

例如编写树莓派的驱动代码:

Linux源码配置  -->  编译得到树莓派专用内核  --> 在树莓派上编译驱动代码

配置的最终目标是为了生成 .config 文件

1、树莓派linux源码配置

在实际的工作中,驱动工程师负责驱动代码的编写。而驱动的编译需要一个提前编译好的内核,编译内核前需要配置,配置的最终生成 .config 文件

获取树莓派 linux 源码

# 查看自己目前树莓派的系统信息
pi@raspberrypi:~$ uname -r
4.19.127-v7

树莓派linux源码
在左上角选择和自己系统版本一样的源码,例如我的系统是 4.19.127-v7 所以我选择 rpi-4.19.y 进行下载。
在这里插入图片描述
下载后拷贝到虚拟机的ubuntu中
在这里插入图片描述

linux源码配置有三种方式,配置前需要安装好交叉编译工具。

方式一:将厂家给的 .config 文件直接拿来用
方式二:基于厂家的 .config 文件一项项进行配置
方式三:完完全全靠自身来,高级工程师的活。

方式一:

树莓派3使用厂家的配置文件是:bcm2709_defconfig

# 在源码文件夹下运行
find . -name *_defconfig | grep bcm2709

在这里插入图片描述
配置命令:

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make bcm2709_defconfig
  • ARCH=arm :指定 ARM 架构
  • CROSS_COMPILE=arm-linux-gnueabihf- :指定编译器
  • KERNEL=kernel7 :树莓派
  • make bcm2709_defconfig :主要核心指令

请添加图片描述
由此厂家的config 变成 .config 配置完成

方式二:

# 先安装一些环境
sudo aptitude install bc -y
sudo aptitude install libncurses5-dev libncursesw5-dev -y 
sudo aptitude install zlib1g:i386 -y 
sudo aptitude install libc6-i386 lib32stdc++6 lib32gcc1 lib32ncurses5 -y 

配置命令:

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make menuconfig

一般选择device

[ * ] built-in:表示编译进了内核,zImage 包含了驱动。

< M > modularizes:表示以模块的方式生成驱动文件 xxx.ko,系统启动后,通过命令inmosd xxx.ko临时加载。

[ * ]< M >是驱动加载的两种方式,可以按空格键进行加载方式的切换。

[ ] :表示略过的,不参与编译,也就是需要裁剪的东西。

2、编译内核

经过前面我们的配置,接下来就是编译内核了。

在源码文件夹下输入编译命令:

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 zImage modules dtbs
  • ARCH=arm :指定ARM平台
  • CROSS_COMPILE=arm-linux-gnueabihf- :指定编译器
  • KERNEL=kernel7 :树莓派
  • -j4 :指定同时用电脑4核运行
  • zImage :生成内核镜像
  • modules :生成驱动模块
  • dtbs :生成配置文件

编译内核需要的时间较长,约15分钟左右……

编译过程没有报错
源码目录中多vmlinux文件
/home/book/linux-rpi-4.19.y/arch/arm/boot 中有zImage文件

在这里插入图片描述

3、挂载新内核到树莓派

首先需要将 zImag 文件打包成树莓派上可用的 .img 文件

#在源码目录下运行

./scripts/mkknlimg arch/arm/boot/zImage ./kernel_new.img   # 生成kernel_new.img 文件

在这里插入图片描述
将树莓派断电,SD卡通过读卡器插到电脑,并挂载在虚拟机上。

# 查看分区是否存在
dmesg

在这里插入图片描述
sde1 和sde2 分别是树莓派sd卡上的两个分区

  • sde1是boot相关的内容,kernel的img文件在此分区
  • sde2是系统跟文件分区

创建两个文件夹

mkdir ~/data1
mkdir ~/data2

将两个分区挂载到新建的文件夹下:

sudo mount /dev/sde1 ~/data1
sudo mount /dev/sde2 ~/data2

然后data1和data2就有数据了
请添加图片描述

在data2中安装设备驱动文件,这个很重要

sudo ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make INSTALL_MOD_PATH=/home/cai/data2 modules_install

在这里插入图片描述

然后就是安装更新kernel7.img文件,为什么一定是叫kernel7.img的文件呢?因为这是树莓派的规定,进入系统前会找到这个文件。为了防止我们更新内核失败,我们先复制一份原本的kernel7.img文件为kernel7_old.img

cp /data1/kernel7.img /data1/kernel7_old.img

然后把前面在源码目录下编译的kernel_new.img拷贝到data1中,替换kernel7.img

cp kernel_new.img ~/data1/kernel7.img

为了提高内核更新的成功率,我们查看一些刚刚拷贝的kernel7.img文件是否成功

md5sum ~/data1/kernel7.img
md5sum ~/linux-rpi-4.19.y/kernel_new.img

如果文件的编码号是一样的,说明拷贝成功了……

除此之外,我们还要拷贝一些东西到data1中

cp arch/arm/boot/dts/.*dtb* /home/cai/data1 && \
cp arch/arm/boot/dts/overlays/.*dtb* /home/cai/data1/overlays/ && \
cp arch/arm/boot/dts/overlays/README /home/cai/data1/overlays/

最后移植完成!!!

为了能看到树莓派系统的启动过程,我们需要打开树莓派的串口登录功能。

打开SD卡的根目录“config.txt”在结尾处添加

dtoverlay=pi3-miniuart-bt

修改根目录下的“cmdline.txt”,将下面的内容全部替换原本的内容

dwc_otg.lpm_enable=0 console=tty1 console=serial0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

最后SD插回树莓派上电,发现我们前面配置的文件加载起来了

# 查看内核版本是否改变
uname -r

在这里插入图片描述
发现内核已发送改变,说明我们内核移植成功!!!!

七、文件系统

在我们认为,linux中文件系统就是根目录,但实际上真的是这样吗?
我们平常用的Windowns系统,单独的文件系统由驱动器名称(A,B,C,D等)来标识盘符,一个盘符就是一个分区,而linux中的文件系统管理也是一样的吗?

在linux对文件的操作都是采用 open()、close()、read()、write()等函数进行操作,这与文件系统又有什么关联呢?

1、文件系统概念(百度)

文件系统(FS):是指用来方便管理文件和组织数据的一种方法。换句话说就是文件系统可以看作是一种程序,这个程序的功能是对存储设备的扇区进行管理,对存储设备的扇区的访问变成了对目录和文件名的访问。在应用层我们利用AIP函数进行访问特定的目录或文件时,文件系统就会将这个文件名或目录转换成扇区号进行访问。

Windows下的文件系统类型:

FAT、FAT32、NTFS等

linux下的文件系统类型:

Ext2,Ext3,Ext4,vfat,jffs,ramfs,nfs等

不同是文件系统类型其对文件的管理方法也不同,但都是文件系统这个的概念。

2、根文件系统

根文件系统(RFS):是一种特殊的文件系统。该文件系统不仅具有普通文件系统的存储数据的功能,相对于普通的文件系统特殊之处在于它是内核启动时所挂载的第一个文件系统,内核代码的映像文件就保存在根目录下。系统引导启动程序会在根文件系统挂载之后从中把一些初始化脚本和服务加载到内存中去运行。

3、虚拟文件系统

虚拟文件系统(VFS):是一种用于网络环境的分布式文件系统,是允许和操作系统使用不同的文件系统实现的接口。是物理文件系统与服务之间的一个接口层,它对Linux的每个文件系统的所有细节进行抽象,使得不同的文件系统在Linux核心以及系统中运行的其他进程看来,都是相同的。严格说来,VFS并不是一种实际的文件系统。它只存在于内存中,不存在于任何外存空间。VFS在系统启动时建立,在系统关闭时消亡。

在linux中,每个分区可能是用不同的文件系统进行管理。每个不同的文件系统的管理方法都不一样,那么我们就需要利用虚拟文件系统(VFS)进行统一的管理。这样我们用相同的API就能直接控制不同分区不用文件系统的数据,所以有利于应用层的开发。

VFS常用的API有:

mount()
umount()

open()
close()

mkdir()

4、文件系统的结构

请添加图片描述

linux的文件系统结构:用户层 、内核层、硬件层

  • 用户层:系统提供一些API方便用户进行对文件的管理,比如open()、wirite()、read()、close()等函数。用户不用了解底层是如何对文件进行操作,只需要通过相对于的API就能完成操作
  • 内核层:内存层中包含有包含了各种设备驱动,对应用层输入的API函数进行解释、操作。同时还是连接硬件的桥梁,通过编写驱动程序,然后封装好API在应用层很容易的调用。
  • 硬件层:硬件层是包含设备的本身的硬件,比如usb、网口、IO口模块、HDMI显示模块等等。

5、常见的目录解释

/bin 二进制可执行命令
/dev 设备特殊文件

/etc 系统管理和配置文件
/etc/rc.d 启动的配置文件和脚本
/home 用户主目录的基点,比如用户user的主目录就是/home/user,可以用~user表示
/lib 标准程序设计库,又叫动态链接共享库,作用类似windows里的.dll文件
/sbin 系统管理命令,这里存放的是系统管理员使用的管理程序
/tmp 公用的临时文件存储点
/root 系统管理员的主目录(呵呵,特权阶级)
/mnt 系统提供这个目录是让用户临时挂载其他的文件系统。
/lost+found 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows下叫什么.chk)就在这里
/proc 虚拟的目录,是系统内存的映射。可直接访问这个目录来获取系统信息。
/var 某些大文件的溢出区,比方说各种服务的日志文件
/usr 最庞大的目录,要用到的应用程序和文件几乎都在这个目录。其中包含:
   /usr/X11R6 存放X window的目录
   /usr/bin 众多的应用程序
   /usr/sbin 超级用户的一些管理程序
   /usr/doc linux文档
   /usr/include linux下开发和编译应用程序所需要的头文件
   /usr/lib 常用的动态链接库和软件包的配置文件
   /usr/man 帮助文档
   /usr/src 源代码,linux内核的源代码就放在/usr/src/linux里
   /usr/local/bin 本地增加的命令
   /usr/local/lib 本地增加的库

八、linux系统结构

Linux系统一般有4个主要部分:内核、shell、文件系统和应用程序。 内核、shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并使用系统。
请添加图片描述

1、内核

内核是与计算机硬件接口的易替换软件的最低级别,它负责将所有以“用户模式”运行的应用程序连接到物理硬件,并允许称为服务器的进程使用进程间通信(IPC)彼此获取信息。

内核是操作系统的核心,具有很多最基本功能,它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性。

Linux内核分为:内存管理、进程管理、设备驱动程序、文件系统和网络管理等。
请添加图片描述

2、用户态和内核态

应用程序是无法直接访问硬件资源的,需要通过内核的驱动才能访问硬件资源。
linux系统将自身分为两部分,一部分是核心软件(kernel),也就是内核空间;另一部分是普通应用程序,也就是用户空间。

  • 内核态,运行于进程上下文,内核代表进程运行于内核空间
  • 内核态,运行于中断上下文,内核代表硬件运行于内核空间
  • 用户态,运行于用户空间

九、linux驱动开发

1、驱动框架

基于pin4引脚的一个驱动开发基本框架,暂时没有实现什么功能,以后的开发都是以这种模板进行修改。

/*pin4_driver.c*/
#include <linux/fs.h>		 //file_operations声明
#include <linux/module.h>    //module_init  module_exit声明
#include <linux/init.h>      //__init  __exit 宏定义声明
#include <linux/device.h>	 //class  devise声明
#include <linux/uaccess.h>   //copy_from_user 的头文件
#include <linux/types.h>     //设备号  dev_t 类型声明
#include <asm/io.h>          //ioremap iounmap的头文件

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";   //模块名

//pin4_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
    printk("pin4_open\n");  //内核的打印函数,和printf类似 
    return 0;
}

//pin4_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
	printk("pin4_write\n");
    return 0;
}

static struct file_operations pin4_fops = {

    .owner = THIS_MODULE,
    .open  = pin4_open,
    .write = pin4_write,
};

int __init pin4_drv_init(void)     // 驱动函数的入口
{

    int ret;
    devno = MKDEV(major,minor);  //创建设备号
    ret   = register_chrdev(major, module_name,&pin4_fops);  
    //注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中

    pin4_class=class_create(THIS_MODULE,"myfirstdemo");
    pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name);  //创建设备文件

    return 0;
}

void __exit pin4_drv_exit(void)
{
    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");

linux 一切皆是文件,因此访问设备和访问文件一样,open(),write(),read()等函数进行操作。

// pin4_main.c    应用层测试
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main()
{
	int fd;
	fd = open("/dev/pin4",O_RDWR);
	if(fd < 0){
		printf("open failed\n");
		perror("reson");
	}else{
		printf("open success\n");
	}
	fd = write(fd,'1',1);//写一个字符'1',写一个字节
	
	return 0;
}

2、驱动模块的编译

准备工作:

  • 前面我们移植的树莓派内核,此次驱动模块的编译需要在虚拟机上源码目录下的/driver/char进行。
  • 交叉编译工具,因为是为树莓派进行编译

将pin4_driver.c拷贝到源码目录的/driver/char下
在这里插入图片描述
然后编辑Makefile文件,添加 obj-m += pin4_driver.o
在这里插入图片描述

然后来到源码的目录下进行编译驱动

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 modules

发现没有这个命令与前面有个命令很像

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 zImage modules dtbs

他们之间的区别就是少了 zImage 和 dtbs
意思是不生成内核镜像,不生成配置文件

如果生成了pin4driver.ko说明编译成功了
在这里插入图片描述
接下来我们编译一下应用层程序 pin4_main.c
在这里插入图片描述

3、测试驱动模块

将pin4_main和pin4driver.ko文件传输到树莓派
在这里插入图片描述
进行驱动的装载:

sudo insmod pin4_drive.ko

在这里插入图片描述
最后运行一下上层测试代码
在这里插入图片描述

sudo chmod 666 /dev/pin4
权限:666代表任何用户都可以访问

在这里插入图片描述
看到提示驱动已经被打开了,我们看一下内核层有没有反应
在这里插入图片描述
有这样的输出,说明驱动成功的被调用了。

假如我们不需要这个驱动了,如何卸载?

sudo rmmod xxx   # 不用写ko

# 查看有哪些内核模块
lsmod

十、I/O驱动开发

通过配置寄存器实现pin17的驱动编写,达到控制它输出高低电平
驱动的开发必须依靠芯片手册,其他平台的芯片开发也是如此。

树莓派3b的芯片是。
利用命令gpio readall 查看引脚信息
在这里插入图片描述

通过查看芯片手册,参考文章树莓派IO口操作
我们只需要三个寄存器就可以完成IO口的输出高低电平

  • GPFSELn (n:0~5):GPIO功能选择寄存器
  • GPSET0,GPSET1:GPIO引脚输出设置寄存器
  • GPCLR0,GPCLR1:GPIO输出清除寄存器
GPIO功能选择寄存器:一共有六组,对应54个GPIO
GPIO引脚输出设置寄存器
	GPSET0: pin0~pin31的设置寄存器,1位高电平,0为低电平,复位后为0
 	GPSET1: pin32~pin53的设置寄存器,1位高电平,0为低电平,复位后为0。
GPIO输出清除寄存器

驱动代码:

//pin17driver.c
#include <linux/fs.h>		 //file_operations声明
#include <linux/module.h>    //module_init  module_exit声明
#include <linux/init.h>      //__init  __exit 宏定义声明
#include <linux/device.h>	 //class  devise声明
#include <linux/uaccess.h>   //copy_from_user 的头文件
#include <linux/types.h>     //设备号  dev_t 类型声明
#include <asm/io.h>          //ioremap iounmap的头文件

static struct class *pin17_class;  
static struct device *pin17_class_dev;

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

volatile unsigned int* GPFSEL1 = NULL;
volatile unsigned int* GPSET0  = NULL;
volatile unsigned int* GPCLR0  = NULL;

//pin17_open函数
static int pin17_open(struct inode *inode,struct file *file)
{
    printk("pin17_open\n");  //内核的打印函数,和printf类似
    
    //open的时候配置pin17为输出引脚
    *GPFSEL1 &= ~(0x6 << 21);
	*GPFSEL1 |= (0x1 << 21);
    
    return 0;
}

//pin17_write函数
static ssize_t pin17_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
	int userCmd;//上层写的是整型数1,底层就要对应起来用int.如果是字符则用char

	printk("pin17_write\n");
	
	//获取上层write的值
	copy_from_user(&userCmd,buf,count);//用户空间向内核空间传输数据
	
	//根据值来执行操作
	if(userCmd == 1){
		printk("set 1\n");
		*GPSET0 |= 0x1 << 17;//设置pin17口为1
	}else if(userCmd == 0){
		printk("set 0\n");
		*GPCLR0 |= 0x1 << 17;//清除pin17口
	}else{
		printk("cmd error\n");
	}
	
    return 0;
}

static struct file_operations pin17_fops = {

    .owner = THIS_MODULE,
    .open  = pin17_open,
    .write = pin17_write,
};

int __init pin17_drv_init(void)   //驱动的真正入口
{

    int ret;
    printk("insmod driver pin17 success\n");
    devno = MKDEV(major,minor);  //创建设备号
    ret   = register_chrdev(major, module_name,&pin17_fops);  //注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中

    pin17_class=class_create(THIS_MODULE,"myfirstdemo");  //由代码在/dev下自动生成设备
    pin17_class_dev =device_create(pin17_class,NULL,devno,NULL,module_name);  //创建设备文件

	GPFSEL1 = (volatile unsigned int *)ioremap(0x3f200004,4);
	GPSET0  = (volatile unsigned int *)ioremap(0x3f20001C,4);
	GPCLR0  = (volatile unsigned int *)ioremap(0x3f200028,4);
 	//虚拟地址映射
 	
 	return 0;
}

void __exit pin17_drv_exit(void)//可以发现和init刚好是相反的执行顺序。
{
	/*退出程序,解除虚拟地址映射*/
	iounmap(GPFSEL1);
	iounmap(GPSET0);
	iounmap(GPCLR0);
	//解除虚拟地址映射
	
    device_destroy(pin17_class,devno);
    class_destroy(pin17_class);
    unregister_chrdev(major, module_name);  //卸载驱动

}

module_init(pin17_drv_init);  //入口:内核加载驱动的时候,这个宏会被调用,而真正的驱动入口是它调用的函数
module_exit(pin17_drv_exit);
MODULE_LICENSE("GPL v2");

定义寄存器

pin17引脚在GPFSEL1分组,引脚输出寄存器在GPSETO分组。

// volatile 关键字的作用是防止编译器优化这些寄存器变量
volatile unsigned int* GPFSEL1 = NULL;		
volatile unsigned int* GPSET0  = NULL;
volatile unsigned int* GPCLR0  = NULL;

初始化寄存器虚拟地址

请添加图片描述
此时 0x7E200004 是总线地址,需要加上偏移地址才是真正的物理地址

IO口的起始地址是0x3f000000,加上GPIO的偏移量0x2000000
所以GPIO的实际物理地址应该是从0x3f200000开始的。

故三个寄存器的地址是:

GPFSEL1 			0x3f200004          物理地址
GPSET0 				0x3f20001C			物理地址
GPCLR0  			0x3f200028			物理地址

物理地址转虚拟地址

我们上层是不能直接控制物理地址,所以我们需要给物理地址映射成虚拟地址

// 物理地址转虚拟地址 API
void *ioremap(unsigned long phys_addr, unsigned long size);

所以三个寄存器的虚拟地址是

GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200004,4);
GPSET0  = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0  = (volatile unsigned int *)ioremap(0x3f200028,4);

引脚功能模式配置

配置pin17引脚为输出模式

*GPFSEL1 &= ~(0x6 << 21);		// 22 23位等于0
*GPFSEL1 |= (0x1 << 21);		// 21 位等于1

引脚输出电平控制

*GPSET0 |= 0x1 << 17;			//设置pin17口为1
*GPCLR0 |= 0x1 << 17;			//清除pin17口为0

上层代码

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

int main()
{
        int fd;
        int cmd;
        
        fd=open("/dev/pin17",O_RDWR);
        if(fd<0){
                perror("reson");
                return -1;
        }
        
        printf("input:1/0(1:高电平,0:低电平)\n");
        scanf("%d",&cmd);
        
		if(cmd == 0){
                printf("pin17设置成低电平\n");
        }else if(cmd == 1){
                printf("pin17设置成高电平\n");
        }
        
        fd=write(fd,&cmd,sizeof(int));
        
        return 0;
}

/*
将pin17设置为输出模式,内核接收上层指令,达到控制输出高低电平的目的
*/

然后在/home/book/linux-rpi-4.19.y/drivers/char下修改 Makefile 文件,然后在源码目录下编译

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 modules

将pin17driver.ko移植到树莓派,在树莓派上运行pin17_main.c
在这里插入图片描述
IO口的驱动成功运行,测试成功!!!!!

树莓派开发到此结束,后期如果有更新会及时发布!

在此感谢博主^不加糖^的博文,在驱动的开发一定程度上参考了这位博主。
参考博文:
https://blog.csdn.net/weixin_44742824/category_10788288.html?spm=1001.2014.3001.5482

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值