树莓派基本开发
连接方式1、串口1、打开SD卡根目录的"config.txt"文件,将以下内容添加在最后并且保存。dtoverlay=pi3-miniuart-bt这样就停止了蓝牙,解除了对串口的占用。2、然后再修改根目录的"cmdline.txt",将里面的内容全部替换成以下内容,以防万一,请先备份好这个文件的原内容。dwc_otg.lpm_enable=0 console=tty1 console=seria
一、树莓派基本配置
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 换源
- 永久的换源
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
- 暂时的换源下载
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
[ * ] 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
更多推荐
所有评论(0)