版权声明
本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。
Copyright (C) 2023 凌云物网智科实验室·郭工
Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292
树莓派硬件开发篇
树莓派开发板之所以如此受欢迎,并且也称为很多极客的玩物,甚至很多公司都会直接用树莓派做产品,或设计类似的产品,最主要的原因是它提供了一组40 Pin的扩展引脚。在这40Pin的扩展引脚上,他提供了常用的硬件接口:
分别提供了两个 5V 和 3.3V的电源,以及8个GND,这为外接其他模块供电提供了极大的方便(大部分的芯片都是 5V或3.3v的供电);
树莓派上提供了丰富的扩展接口,它包括GPIO、PWM、1-Wire、I2C、UART、SPI接口等,这样就可以通过这些接口接各种各样的硬件外设或传感器;
树莓派上的这些引脚可以通过 DTOverlay(Device Tree Overlay)的技术,来动态配置这些GPIO引脚究竟工作在哪种模式
下面是树莓派硬件的40Pin引脚功能示意图,不论你买的是哪个版本的树莓派,他们都遵循这个统一的40Pin引脚规范。甚至这个引脚定义都称为了一个行业标准,很多其它厂商在推出类似的开发板时,都会做类似的兼容参考设计。所以如果我们打算购买一个树莓派来学习嵌入式Linux系统硬件开发时,其实买哪个版本的树莓派区别不大,这个可以根据自己的实际预算来选择。
在新版本的Raspbian系统中添加了查看树莓派40Pin引脚定义的 pinout
命令,如果没有安装的话,可以使用下面命令安装。
pi@raspberrypi:~ $ sudo apt install -y python3-gpiozero
通过该命令我们可以在命令行中查看相应的引脚定义,如下图所示:
关于树莓派的40Pin定义及使用,我们也可以参考树莓派的40Pin定义官方文档。接下来本篇我们将以树莓派上的40Pin引脚为例,详细讲解在Linux系统下如何使用相应的接口进行应用程序编程。
1. 树莓派交叉编译
1.1 树莓派交叉编译
1.1.1 树莓派本地编译
树莓派拥有丰富的硬件资源,如树莓派4B配备有4核1.5GHz ARM64位处理器,另外还有 2GB/4GB/8GB 版本内存。除此以外,树莓派系统上默认安装有 vi、gcc、make 的常用C程序开发工具,这就使得我们在树莓派上直接开发C程序成为可能。接下来,我们以 Hello 程序为例,讲解树莓派上直接开发C程序。
如果默认没有安装 vim 命令,我们还是安装熟悉的 vim,当然大家也可以使用默认的 vi 或 nano 编辑器替代。如果默认没有安装 gcc 也一起安装了。
guowenxue@Raspberrypi4B:~ $ sudo apt install -y vim gcc
接下来使用 vim 编写 hello.c 程序如下.
guowenxue@Raspberrypi4B:~ $ vim hello.c
/*********************************************************************************
* Copyright: (C) 2023 Guo Wenxue<guowenxue@gmail.com>
* All rights reserved.
*
* Filename: hello.c
* Description: This file is hello world C program
*
* Version: 1.0.0(29/07/23)
* Author: Guo Wenxue <guowenxue@gmail.com>
* ChangeLog: 1, Release initial version on "29/07/23 10:45:07"
*
********************************************************************************/
#include <stdio.h>
int main (int argc, char **argv)
{
printf("Hello, LingYun IoT Studio!\n");
return 0;
}
完成C程序编写后,使用 gcc 编译生成可执行程序.
guowenxue@Raspberrypi4B:~ $ gcc hello.c -o hello
使用 file 命令可以查看编译生成的 hello 程序类型,如果默认没有安装 file 命令则可以 apt install 安装。
guowenxue@Raspberrypi4B:~ $ sudo apt install file
guowenxue@Raspberrypi4B:~ $ file hello
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, BuildID[sha1]=fe077f99ebe322a125927bcbe6ab614ae3b133a5, for GNU/Linux 3.2.0, not stripped
从上面的输出我们可以看出:
编译生成的hello是 32位、小端字节序、ELF格式的可执行文件;
该程序应该运行在ARM处理器使用,使用 EABI5 的接口;
程序是动态链接的,这样它在运行的时候依赖一些其他的动态库;
该程序由 /lib/ld-linux-armhf.so.3 来加载解释运行;
该程序兼容Linux内核 3.2.0,并且没有使用 strip 命令去掉一些不用的信息;
最后,我们执行该程序就可以看到 Hello 输出.
guowenxue@Raspberrypi4B:~ $ ./hello
Hello, LingYun IoT Studio!
由此可见,在树莓派上开发这种简单的C程序,跟Linux服务器上的开发没有太大的区别。
1.1.2 交叉编译器安装
在 X86 架构 Linux 系统下进行 C 程序开发时, 我们直接使用gcc 编译器进行代码的编译, 编译生成的可执行程序直接在 X86 架构下的 PC 下运行的,这个过程叫做 本地编译 (Native Compile) 。像上面我们直接在树莓派系统上编写代码并编译运行,这个过程也是本地编译。
但并不是所有的ARM开发板都具备这么强大的硬件性能,如有些ARM开发板只有400MHz的单核处理器、256MB的外存和64MB的内存空间,在这样的系统上就不可能安装 gcc、vim等C开发环境。即使有些开发板硬盘和内存空间足够大,但CPU处理能力不强,这样编译大型的开源代码也会非常非常慢,如树莓派设备上源码编译Linux内核源码、OpenCV、QT等大型开源软件时就不大合适了。
这种情况下,我们通常会在硬件性能比较强劲的X86 Linux服务器上编写、编译源代码,而编译生成的动态库或可执行文件却放到ARM开发板上去运行。这样在一钟平台上编译出能在另外一种体系结构完全不同处理器上运行程序的编译过程,叫做 交叉编译 (Cross Compile)。
如果想要将C程序源码编译生成ARM处理器系统上运行的程序, 则必须要使用ARM的编译器来编译, 这个编译器我们通常称为 交叉编译器 (Cross Compiler)。通常 ARM开发板不同,其使用的 ARM处理器也可能不同;而 ARM处理器架构不同,则其交叉编译器也不一样。这样,不同的开发板就有可能需要安装不同的交叉编译器了。
1.1.2.1 ARM官方交叉编译器安装
众所周知,树莓派开发板使用的是ARM处理器,而 ARM开发者官方 提供了各个版本ARM处理器通用的交叉编译器,这样我们可以直接从这里下载不同版本树莓派系统对应的交叉编译器,如当前最新的 Bullseye 树莓派系统推荐使用 9.2-2019.12。此外,从树莓派3B开始使用的是博通(Broadcom) 64位 ARMv8 处理器,而官方的树莓派操作系统也提供了 32位 和 64位两个版本。
如果树莓派上安装的是32位系统,则应该下载 AArch32 target with hard float (arm-none-linux-gnueabihf) 交叉编译器;
如果树莓派上安装的是64位系统,则应该下载 AArch64 GNU/Linux target (aarch64-none-linux-gnu) 交叉编译器;
这里以32位树莓派系统 Bullseye为例,讲解Linux下该交叉编译器的下载使用方式。首先使用 wget 命令从ARM官方下载交叉编译器,因为我的Ubuntu服务器为64位系统,所以下载 X86_64 服务器版本。
guowenxue@ubuntu20:~$ wget https://developer.arm.com/-/media/Files/downloads/gnu-a/9.2-2019.12/binrel/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf.tar.xz
如果有 root 权限的话,推荐将交叉编译器解压缩安装到 /opt 路径下,并重命名为 gcc-aarch32-9.2-2019.12.
guowenxue@ubuntu20:~$ sudo tar -xJf gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf.tar.xz -C /opt/
guowenxue@ubuntu20:~$ sudo mv /opt/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/ /opt/gcc-aarch32-9.2-2019.12
否则将交叉编译器解压缩安装到自己的 $HOME/crosstool/ 路径下.
guowenxue@ubuntu20:~$ mkdir -p $HOME/crosstool/
guowenxue@ubuntu20:~$ tar -xJf gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf.tar.xz -C $HOME/crosstool/
guowenxue@ubuntu20:~$ mv $HOME/crosstool/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/ $HOME/crosstool/gcc-aarch32-9.2-2019.12
接下来可以运行下面命令来测试该交叉编译器是否安装完成.
guowenxue@ubuntu20:~$ $HOME/crosstool/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-gcc -v
或
guowenxue@ubuntu20:~$ /opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-gcc -v
gcc version 9.2.1 20191025 (GNU Toolchain for the A-profile Architecture 9.2-2019.12 (arm-9.10))
如果每次都使用绝对路径来使用交叉编译器会比较繁琐,此时我们可以将交叉编译器的路径添加到 bash shell的默认配置文件 ~/.bashrc 中的 PATH 环境变量中,这样就可以直接使用交叉编译器命令了。
guowenxue@ubuntu20:~$ vim ~/.bashrc
export PATH=/opt/gcc-aarch32-9.2-2019.12/bin/:$PATH
或
export PATH=$HOME/crosstool/gcc-aarch32-9.2-2019.12/bin/:$PATH
接下来使用 source 命令让该配置立即生效,这样就可以直接使用交叉编译器命令进行测试了.
guowenxue@ubuntu20:~$ source ~/.bashrc
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-gcc -v
... ...
gcc version 10.3.1 20210621 (GNU Toolchain for the A-profile Architecture 10.3-2021.07 (arm-10.29))
1.1.2.2 Ubuntu交叉编译器安装
之所以嵌入式Linux开发通常都会选择 Ubuntu 系统作为首选开发环境,这是因为 Ubuntu 系统的强大软件仓库提供了各种各样嵌入式软件开发所需要的工具软件。同样的,树莓派的交叉编译器也可以直接使用 apt install
命令安装。
由于Ubuntu 22.04 提供的默认交叉编译器 gcc version 11.3.0 里所使用的glibc库版本过高,这样将会导致该版本编译器编译的C程序放到树莓派 Bullseye 系统上运行时,将会出现如下错误:
guowenxue@pubraspberry:~ $ ./hello
./hello: /lib/arm-linux-gnueabihf/libc.so.6: version `GLIBC_2.34' not found (required by ./hello)
所以 Ubuntu 22.04 版本推荐使用上面的方法下载安装,而 Ubuntu 20.04 版本则可以直接使用 apt install
命令安装。如果安装的是 32 位树莓派系统,则使用下面命令安装32位的~~树~~莓派的C/C++交叉编译器.
guowenxue@ubuntu20:~$ sudo apt install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-gcc -v
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-g++ -v
gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
如果安装的是 64 位树莓派系统,则使用下面命令安装64位的树莓派C/C++交叉编译器.
guowenxue@ubuntu20:~$ sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
guowenxue@ubuntu20:~$ aarch64-linux-gnu-gcc -v
guowenxue@ubuntu20:~$ aarch64-linux-gnu-g++ -v
gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
1.1.3 交叉编译器链介绍
通常编译工具链由编译器、链接器和解释器构成,具体到组件上是由Binutils、GCC、Glibc和GDB构成的。
guowenxue@ubuntu20:~$ sudo dpkg -L gcc-arm-linux-gnueabihf | grep /usr/bin/
/usr/bin/arm-linux-gnueabihf-gcc
/usr/bin/arm-linux-gnueabihf-gcc-ar
/usr/bin/arm-linux-gnueabihf-gcc-nm
/usr/bin/arm-linux-gnueabihf-gcc-ranlib
/usr/bin/arm-linux-gnueabihf-gcov
/usr/bin/arm-linux-gnueabihf-gcov-dump
/usr/bin/arm-linux-gnueabihf-gcov-tool
guowenxue@ubuntu20:~$ sudo dpkg -L g++-arm-linux-gnueabihf | grep /usr/bin/
/usr/bin/arm-linux-gnueabihf-g++
guowenxue@ubuntu20:~$ sudo dpkg -L binutils-arm-linux-gnueabihf | grep /usr/bin/
/usr/bin/arm-linux-gnueabihf-addr2line
/usr/bin/arm-linux-gnueabihf-ar
/usr/bin/arm-linux-gnueabihf-as
/usr/bin/arm-linux-gnueabihf-c++filt
/usr/bin/arm-linux-gnueabihf-dwp
/usr/bin/arm-linux-gnueabihf-elfedit
/usr/bin/arm-linux-gnueabihf-gprof
/usr/bin/arm-linux-gnueabihf-ld.bfd
/usr/bin/arm-linux-gnueabihf-ld.gold
/usr/bin/arm-linux-gnueabihf-nm
/usr/bin/arm-linux-gnueabihf-objcopy
/usr/bin/arm-linux-gnueabihf-objdump
/usr/bin/arm-linux-gnueabihf-ranlib
/usr/bin/arm-linux-gnueabihf-readelf
/usr/bin/arm-linux-gnueabihf-size
/usr/bin/arm-linux-gnueabihf-strings
/usr/bin/arm-linux-gnueabihf-strip
/usr/bin/arm-linux-gnueabihf-ld
下表列出了交叉编译器链中各个工具的作用:
工具名 |
工具说明 |
---|---|
gcc |
C程序源码编译前端工具,它会调用Binutils提供的工具来对源码进行预处理、编译、汇编、最后链接生成可执行文件 |
g++ |
C++程序源码编译前端工具,它会调用Binutils提供的工具来对源码进行预处理、编译、汇编、最后链接生成可执行文件 |
cpp |
C程序预处理器(C preprocessor) |
as |
该工具用来将汇编源码汇编成目标机器代码.o文件 |
ar |
该工具用来将多个可重定位的.o文件归档为一个静态库.a文件 |
ranlib |
产生归档.a文件索引,并将其保存到这个归档文件中,因为 ar 命令支持该特性,所以现在很少使用了 |
ld |
链接器,用来将多个目标文件.o、静态库.a文件、动态库.so 文件链接生成一个可执行文件 |
readelf |
列出 ELF 格式可执行文件的相关信息 |
nm |
列出目标文件中的函数符号表 |
size |
列出目标文件中每个段(text、data、bss等)的大小 |
strings |
列出目标文件中能打印出来的字符串,如代码中的字符串常量”Hello, World”,”Password”等 |
strip |
去掉目标文件中一些无关调试信息等,这样可以减小文件的大小 |
objcopy |
把一种目标文件中的内容复制到另一种目标文件中,裸机开发经常会用这个命令将ELF格式的文件转换成二进制文件 |
objdump |
该工具常用于对二进制文件进行反汇编,默认输出到标准输出,所以一般配合重定向一起使用 |
addr2line |
该工具可以将程序地址换为文件名、函数名和源代码行号,主要用来调试或反汇编 |
使用交叉编译器 arm-linux-gnueabihf-gcc 交叉编译生成 hello 程序
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-gcc hello.c -o hello
可以使用 file 命令查看 hello 程序的相关信息.
guowenxue@ubuntu20:~$ file hello
hello: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, BuildID[sha1]=2874be72251c350c355f9ce98b7b5f99016b4a6a, for GNU/Linux 3.2.0, not stripped
使用 arm-linux-gnueabihf-ar 工具制作静态库文件.
guowenxue@ubuntu20:~$ vim file1.c
int func_add(int a, int b)
{
return a+b;
}
guowenxue@ubuntu20:~$ vim file2.c
int func_sub(int a, int b)
{
return a-b;
}
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-gcc -c file1.c file2.c
guowenxue@ubuntu20:~$ ls file*.o
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-ar -rcs libalg.a file1.o file2.o
使用 arm-linux-gnueabihf-readelf 工具查看 hello 程序 ELF信息.
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-readelf -a hello
使用 arm-linux-gnueabihf-nm 工具查看 hello 程序符号表。
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-nm hello
使用 arm-linux-gnueabihf-strings 工具显示可执行程序中能打印出来的字符串,如代码中的字符串常量 “Hello, World”等.
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-strings hello
使用 arm-linux-gnueabihf-size 工具查看 hello 程序各个段大小,单片机裸机开发环境(如STM32CubeIDE)在编译生成可执行文件后,通常会使用该工具列出相关段信息。
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-size hello
text data bss dec hex filename
1102 324 4 1430 596 hello
使用 arm-linux-gnueabihf-objcopy 工具将 ELF 可执行文件转换成单片机Flash烧写的 binary格式.bin文件 或 摩托罗拉 .srec 格式文件。单片机裸机开发环境(如STM32CubeIDE)在编译生成ELF文件后会将其转换成 .bin文件.
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-objcopy -O binary hello hello.bin
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-objcopy -O srec hello hello.srec
使用 arm-linux-gnueabihf-objdump 工具反汇编 hello 程序
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-objdump -D hello > hello.s
使用 arm-linux-gnueabihf-strip 工具去掉 hello 调试信息,可以看到文件明显变小。
guowenxue@ubuntu20:~$ ls -l hello
-rwxrwxr-x 1 guowenxue guowenxue 8152 Jul 31 14:50 hello
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-strip hello
guowenxue@ubuntu20:~$ ls -l hello
-rwxrwxr-x 1 guowenxue guowenxue 5524 Jul 31 14:59 hello
guowenxue@ubuntu20:~$ file hello
hello: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, BuildID[sha1]=2874be72251c350c355f9ce98b7b5f99016b4a6a, for GNU/Linux 3.2.0, stripped
1.1.4 树莓派C程序交叉编译
首先,在 Ubuntu 服务器上使用 vim 编写 hello.c 程序代码。
guowenxue@ubuntu20:~$ vim hello.c
/*********************************************************************************
* Copyright: (C) 2023 Guo Wenxue<guowenxue@gmail.com>
* All rights reserved.
*
* Filename: hello.c
* Description: This file is hello world C program
*
* Version: 1.0.0(29/07/23)
* Author: Guo Wenxue <guowenxue@gmail.com>
* ChangeLog: 1, Release initial version on "29/07/23 10:45:07"
*
********************************************************************************/
#include <stdio.h>
int main (int argc, char **argv)
{
printf("Hello, LingYun IoT Studio!\n");
return 0;
}
如果我们使用 Ubuntu X86服务器的 gcc 编译器编译,则生成的可执行文件应该在 Ubuntu X86服务器上运行.
guowenxue@ubuntu20:~$ gcc hello.c -o hello
guowenxue@ubuntu20:~$ file hello
hello: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2e1f794f6b0c60c7319619eb17b470ee8c4a720d, for GNU/Linux 3.2.0, not stripped
guowenxue@ubuntu20:~$ ./hello
Hello, LingYun IoT Studio!
如果我们使用前面安装的ARM交叉编译器编译,则生成的可执行文件应该在树莓派系统上运行.
guowenxue@ubuntu20:~$ /opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-gcc hello.c -o hello
guowenxue@ubuntu20:~$ file hello
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, with debug_info, not stripped
此时我们可以使用 scp 命令将 hello 程序拷贝到树莓派系统上,当然也可以使用其他方法将其从X86 Ubuntu服务器上拷贝到树莓派上。
guowenxue@ubuntu20:~$ scp -P 22 hello guowenxue@192.168.2.40:~
scp scp是secure copy的简写,它用于在两台Linux系统之间使用 SSH2 协议进行远程文件拷贝;
-P 22 指定 SSH2 协议的端口,如果目标主机的 SSH2 服务监听默认的22号端口,则可以省略该选项;
hello 要拷贝的本地文件;
guowenxue@192.168.2.40:~ 要拷贝的目标主机 SSH2 登录帐号和路径;
如果是拷贝文件夹则需要加上 -r 选项;
如果想要把目标主机的文件拷贝到本地则对调两个命令参数即可;
接下来,在树莓派上直接运行 hello 程序即可.
guowenxue@Raspberrypi4B:~ $ ./hello
Hello, LingYun IoT Studio!
1.1.4 Makefile交叉编译
在使用Makefile 来编译生成可执行文件的时候,我们需要在Makefile里指定交叉编译器。编译 hello 程序的 Makefile 如下所示:
guowenxue@ubuntu20:~$ vim Makefile
CROSS_COMPILE=/opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-
CC=${CROSS_COMPILE}gcc
all:
${CC} hello.c -o hello
该 Makefile 文件解读如下:
第1行定义了变量 CROSS_COMPILE,用来指定交叉编译器所在路径及其前缀;
第2行定义了变量CC,它由 CROSS_COMPILE 和 gcc两部分组成,此时扩展为交叉编译器 /opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-gcc 。而如果把上面的 CROSS_COMPILE 变量定义注释掉,则其扩展为 gcc,这时候就编译给 X86 了;
第3行定义了一个总的目标 all;
第4行定义了完成总目标 all 的动作,即使用交叉编译器编译 hello.c 源码生成 hello 可执行程序;
下面是该 Makefile 编译的执行结果.
guowenxue@ubuntu20:~$ make
/opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-gcc hello.c -o hello
guowenxue@ubuntu20:~$ file hello
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, with debug_info, not stripped
如果我们想在 Makefile 中编译生成静态库,则可以参考下面的实现方式:
guowenxue@ubuntu20:~$ vim file1.c
int func_add(int a, int b)
{
return a+b;
}
guowenxue@ubuntu20:~$ vim file2.c
int func_sub(int a, int b)
{
return a-b;
}
guowenxue@ubuntu20:~$ vim Makefile
CROSS_COMPILE=/opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-
CC=${CROSS_COMPILE}gcc
AR=${CROSS_COMPILE}ar
libs:
${CC} -c file1.c
${CC} -c file2.c
${AR} -rcs libalg.a file1.o file2.o
@rm -f *.o
下面是该 Makefile 编译的执行结果.
guowenxue@ubuntu20:~$ make
/opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-gcc -c file1.c
/opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-gcc -c file2.c
/opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-ar -rcs libalg.a file1.o file2.o
guowenxue@ubuntu20:~$ file libalg.a
libalg.a: current ar archive
1.2 开源应用程序移植
在做嵌入式Linux系统开发时,通常我们需要移植很多开源库,如嵌入式数据库sqlite、Linux系统GPIO标准库libgpiod、标准物联网通信协议Mosquitto、图形化界面开发库QT、图像识别开源库OpenCV等。通常,这些开源软件都提供源代码,如果我们想让这些开源库能够在目标开发板上使用的话,则必须要使用目标开发板的交叉编译器来编译。
所以开源应用程序移植,也是嵌入式Linux开发必备的技能之一。这里所说的移植,其实就是将开源库的源代码下载下来,然后使用交叉编译器编译并能够在目标开发板上运行或使用的过程。通常,这些开源软件最终都会使用Makefile来编译或安装,但Makefile的生成一般会有下面几种方式:
源码直接提供了Makefile 文件;
由configure脚本生成Makefile;
由 autogen 脚本生成Makefile;
1.2.1 源码直接提供Makefile文件
有些开源软件代码里直接提供了Makefile文件,如Linux系统下的 tree 命令源代码就是这样。这里以 tree 命令为例讲解这种类型的源码移植过程,其下载地址为: https://www.linuxfromscratch.org/blfs/view/svn/general/tree.html
下载解压缩 tree 命令源码并解压缩.
guowenxue@ubuntu20:~$ mkdir -p ~/port && cd ~/port/
guowenxue@ubuntu20:~/port$ wget https://mama.indstate.edu/users/ice/tree/src/tree-2.1.1.tgz
guowenxue@ubuntu20:~/port$ tar -xzf tree-2.1.1.tgz
进入到 tree 命令源码里,可以看到源码里提供了 Makefile 文件.
guowenxue@ubuntu20:~/port$ cd tree-2.1.1/
guowenxue@ubuntu20:~/port/tree-2.1.1$ ls
CHANGES color.c doc file.c filter.c hash.c html.c info.c INSTALL json.c LICENSE list.c Makefile README strverscmp.c TODO tree.c tree.h unix.c xml.c
修改 Makefile 里的 PREFIX 和 CC 变量,如果 Makefile 中有出现 AR、AS、LD、NM、RANLIB、OBJDUMP、STRIP 等变量,也要将它们替换成交叉编译器链中的相应工具。
PREFIX=../install
CROSS_COMPILE=/opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-
CC=${CROSS_COMPILE}gcc
其中:
PREFIX 指定执行 make install 时的目标文件安装路径;
CC 指定交叉编译器;
接下来执行编译、安装:
guowenxue@ubuntu20:~/port/tree-2.1.1$ make
guowenxue@ubuntu20:~/port/tree-2.1.1$ make install
使用 file 命令确认生成的可执行文件是 ARM 版本的。
guowenxue@ubuntu20:~/port/tree-2.1.1$ file ../install/bin/tree
../install/bin/tree: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, with debug_info, not stripped
拷贝到树莓派上运行测试:
guowenxue@ubuntu20:~/port/tree-2.1.1$ scp ../install/bin/tree guowenxue@192.168.2.40:~
guowenxue@Raspberrypi4B:~ $ ./tree -L 1 /
1.2.2 源码里提供Configure脚本
绝大部分的开源软件代码里并没有直接提供 Makefile 文件,但是会有一个用来生成 Makefile文件的 configure Shell脚本文件,如 sqlite 数据库源码就是如此。这里就以 sqlite 数据库为例讲解这种类型的源码移植过程,其下载地址为:https://www.sqlite.org/download.html
首先下载 Sqlite 数据库源码并解压缩:
guowenxue@ubuntu20:~$ mkdir -p ~/port && cd ~/port/
guowenxue@ubuntu20:~/port$ wget https://www.sqlite.org/2023/sqlite-autoconf-3420000.tar.gz
guowenxue@ubuntu20:~/port$ tar -xzf sqlite-autoconf-3420000.tar.gz
进入到 sqlite 命令源码里,可以看到源码里并没有提供 Makefile 文件,而有个 configure 文件。
guowenxue@ubuntu20:~/port$ cd sqlite-autoconf-3420000/
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ ls
aclocal.m4 config.guess configure depcomp install-sh Makefile.am Makefile.in missing Replace.cs sqlite3.1 sqlite3ext.h sqlite3.pc.in sqlite3rc.h
compile config.sub configure.ac INSTALL ltmain.sh Makefile.fallback Makefile.msc README.txt shell.c sqlite3.c sqlite3.h sqlite3.rc tea
在使用 configure 脚本来生成Makefile文件之前,先导出交叉编译器工具链相应的环境变量。
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ export CROSS_COMPILE=/opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ export CC=${CROSS_COMPILE}gcc
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ export AS=${CROSS_COMPILE}as
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ export AR=${CROSS_COMPILE}ar
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ export LD=${CROSS_COMPILE}ld
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ export NM=${CROSS_COMPILE}nm
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ export RANLIB=${CROSS_COMPILE}ranlib
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ export OBJDUMP=${CROSS_COMPILE}objdump
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ export STRIP=${CROSS_COMPILE}strip
接下来执行 configure 脚本,它将会根据这些环境变量来生成相应的 Makefile 文件。
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ ./configure --prefix=`pwd`/../sqlite/ --build=i686-pc-linux --host=arm-linux --enable-static
–prefix 用来指定执行 make install 时的目标文件安装路径,需要注意的是这里必须使用绝对路径;
–build=i686-pc-linux 用来指定交叉编译的宿主机平台,这里为 i686-pc-linux ;
–host=arm-linux 用来指定交叉编译的目标运行平台,这里为 arm-linux ;
–enable-static 用来指定在编译生成动态库的同时,还生成静态库文件;
上面的这些选项基本上是开源源代码 ./confgure 的 通用选项,除此以外不同的软件包还提供其它不同的 feature 选项。在执行这个脚本之前,我们通常会使用 –help 选项查看其使用帮助信息,看看这个软件包可以提供哪些其它的功能或选项。
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ ./configure --help
在 configure 完成之后,它将会生成 Makefile 文件。打开Makefile文件,我们将会看到所有编译器工具都替换成交叉编译器工具链,安装目录 prefix 参数也替换成了我们在 ./configure 时传入的参数:
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ vim Makefile
AR = /opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-ar
CC = /opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-gcc
LD = /opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-ld
NM = /opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-nm
OBJDUMP = /opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-objdump
RANLIB = /opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-ranlib
STRIP = /opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-strip
prefix = /home/guowenxue/port/sqlite-autoconf-3420000/../install
生成 Makefile 文件之后,我们就可以 make, make install 了。
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ make
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ make install
使用 tree 命令看看sqlite 的编译输出
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ tree ../sqlite
../sqlite
├── bin
│ └── sqlite3
├── include
│ ├── sqlite3ext.h
│ └── sqlite3.h
├── lib
│ ├── libsqlite3.a
│ ├── libsqlite3.la
│ ├── libsqlite3.so -> libsqlite3.so.0.8.6
│ ├── libsqlite3.so.0 -> libsqlite3.so.0.8.6
│ ├── libsqlite3.so.0.8.6
│ └── pkgconfig
│ └── sqlite3.pc
└── share
└── man
└── man1
└── sqlite3.1
7 directories, 10 files
查看编译生成的 sqlite 可执行程序格式。
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ file ../sqlite/bin/sqlite3
../sqlite/bin/sqlite3: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, with debug_info, not stripped
将编译输出的文件打包拷贝到树莓派上。
guowenxue@ubuntu20:~/port/sqlite-autoconf-3420000$ cd ..
guowenxue@ubuntu20:~/port$ tar -cjf sqlite.tar.bz2 sqlite
guowenxue@ubuntu20:~/port$ scp sqlite.tar.bz2 guowenxue@192.168.2.40:~
树莓派上解压缩.
guowenxue@Raspberrypi4B:~ $ tar -xjf sqlite.tar.bz2
guowenxue@Raspberrypi4B:~ $ ls sqlite
bin include lib share
接下来我们导出可执行文件和动态库的路径,就可以测试 sqlite 命令了。
guowenxue@Raspberrypi4B:~ $ export LD_LIBRARY_PATH=~/sqlite/lib:$LD_LIBRARY_PATH
guowenxue@Raspberrypi4B:~ $ export PATH=~/sqlite/bin/:$PATH
guowenxue@Raspberrypi4B:~ $ sqlite3 --version
3.42.0 2023-05-16 12:36:15 831d0fb2836b71c9bc51067c49fee4b8f18047814f2ff22d817d25195cf350b0
当然,如果我们有 root 的权限,也可以把 可执行文件拷贝到 /usr/bin 路径下,把动态库文件拷贝到 /usr/lib 下,这样我们就可以直接使用了。
1.2.3 源码里提供autogen脚本
有些开源软件代码里并没有直接提供 Makefile 文件,也没有 configure 脚本文件,但是提供了一个 autogen.sh 的脚本。其中 autogen.sh 用来生成 configure 脚本,而 configure 脚本则用来生成 Makefile 文件。
如Linux系统GPIO标准库 libgpiod 源码就是如此。这里就以 libgpiod 库为例讲解这种类型的源码移植过程,其下载地址为:https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/
首先下载 libgpiod 库源码并解压缩。
guowenxue@ubuntu20:~$ mkdir -p ~/port && cd ~/port/
guowenxue@ubuntu20:~/port$ wget https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/snapshot/libgpiod-2.0.tar.gz
guowenxue@ubuntu20:~/port$ tar -xzf libgpiod-2.0.tar.gz
源码中只有 autogen.sh 脚本,并没有 configure 脚本和 Makefile 文件。
guowenxue@ubuntu20:~/port/libgpiod-2.0$ ls
autogen.sh bindings configure.ac COPYING Doxyfile.in include lib LICENSES Makefile.am man NEWS README tests TODO tools
运行 autogen.sh 脚本用来生成 configure文件,注意在执行该脚本前不能导出交叉编译器。
guowenxue@ubuntu20:~/port/libgpiod-2.0$ ./autogen.sh
guowenxue@ubuntu20:~/port/libgpiod-2.0$ ls configure
接下来再导出交叉编译器。
guowenxue@ubuntu20:~/port/libgpiod-2.0$ export CROSS_COMPILE=/opt/gcc-aarch32-9.2-2019.12/bin/arm-none-linux-gnueabihf-
guowenxue@ubuntu20:~/port/libgpiod-2.0$ export CC=${CROSS_COMPILE}gcc
guowenxue@ubuntu20:~/port/libgpiod-2.0$ export AS=${CROSS_COMPILE}as
guowenxue@ubuntu20:~/port/libgpiod-2.0$ export AR=${CROSS_COMPILE}ar
guowenxue@ubuntu20:~/port/libgpiod-2.0$ export LD=${CROSS_COMPILE}ld
guowenxue@ubuntu20:~/port/libgpiod-2.0$ export NM=${CROSS_COMPILE}nm
guowenxue@ubuntu20:~/port/libgpiod-2.0$ export RANLIB=${CROSS_COMPILE}ranlib
guowenxue@ubuntu20:~/port/libgpiod-2.0$ export OBJDUMP=${CROSS_COMPILE}objdump
guowenxue@ubuntu20:~/port/libgpiod-2.0$ export STRIP=${CROSS_COMPILE}strip
执行 ./configure 脚本用来生成 Makefile文件。
guowenxue@ubuntu20:~/port/libgpiod-2.0$ echo "ac_cv_func_malloc_0_nonnull=yes" > arm-linux.cache
guowenxue@ubuntu20:~/port/libgpiod-2.0$ ./configure --prefix=`pwd`/../install/ --build=i686-pc-linux --host=arm-linux --enable-static --enable-tools --cache-file=arm-linux.cache
–cache-file 选项主要是解决编译 tools时出现的
undefined reference to 'rpl_malloc'
bug;
生成 Makefile 文件之后,我们就可以 make, make install 了。
guowenxue@ubuntu20:~/port/libgpiod-2.0$ make
guowenxue@ubuntu20:~/port/libgpiod-2.0$ make install
使用 file 命令查看交叉编译生成的可执行文件是否是 ARM 格式。
guowenxue@ubuntu20:~/port/libgpiod-2.0$ tree ../install/
../install/
├── bin
│ ├── gpiodetect
│ ├── gpioget
│ ├── gpioinfo
│ ├── gpiomon
│ ├── gpionotify
│ └── gpioset
├── include
│ └── gpiod.h
└── lib
├── libgpiod.a
├── libgpiod.la
├── libgpiod.so -> libgpiod.so.3.0.0
├── libgpiod.so.3 -> libgpiod.so.3.0.0
├── libgpiod.so.3.0.0
└── pkgconfig
└── libgpiod.pc
4 directories, 13 files
guowenxue@ubuntu20:~/port/libgpiod-2.0$ file ../install/bin/gpioget
../install/bin/gpioget: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, with debug_info, not stripped
这里就不放到树莓派上运行测试了。
2. 树莓派GPIO接口
我们知道,大自然色由Red,Green,Blue三原色(或称三基色)组成,在特定的颜色和亮度配比下可以发出可见光谱波段的任意颜色,如RGB三个颜色全部点亮时则会表现出白色。下面是我在淘宝上购买的一个共阳极RGB三色Led灯,其工作电压为 5V,实际上我们外接3.3V电压也可以正常工作。
需要注意的是,通常RGB三色Led灯硬件设计上有两种接法,如下图所示:
共阳极 将所有发光二极管的阳极(VCC)接到一起,这样我们通过编程控制R、G、B引脚 输出低电平时 相应的 Led灯就亮了 ;反之如果我们控制这些引脚 输出高电平 时,相应的 Led灯就灭了 ;
共阴极 将所有发光二极管的阴极(GND)接到一起,这样我们通过编程控制R、G、B引脚 输出高电平时 相应的 Led灯就亮了 ;反之如果我们控制这些引脚 输出低电平 时,相应的 Led灯就灭了 ;
接下来我们将以如何控制这个三色Led灯为例,来讲解Linux系统下的GPIO C语言编程接口及其API。
2.1 硬件连接
2.1.1 硬件连接分析
如下图所示,将 RGB三色Led灯连接到树莓派的40Pin引脚上。这里我们所使用的三色Led灯为共阳极,为方便取电我们就连到了树莓派的 #1(VCC 3.3V)、#3、#5、#7 脚上了,如果大家购买的三色Led灯为共阴极的话,则可以使用下面的 #33、#35、#37、#39(GND) 引脚。
从上面的连接示意图我们可以看出:
RGB三色灯 V(电源) 连接到了 树莓派40Pin 1#脚 3.3V 上;
RGB三色灯 R(红灯) 连接到了 树莓派40Pin 3#脚 GPIO2上;
RGB三色灯 B(蓝灯) 连接到了 树莓派40Pin 5#脚 GPIO3上;
RGB三色灯 G(绿灯) 连接到了 树莓派40Pin 7#脚 GPIO4上;
2.1.2 Python编程控制
接下来我们编写Python测试脚本如下,用来测试三色Led是否能够正常工作。
pi@raspberrypi:~ $ vim led.py
#!/usr/bin/env python
#encoding: utf-8
import RPi.GPIO
import time
# RGB LED <----> PhyPin <----> BCM Pin
#
# V <----> #1 <----> 3.3V
# R <----> #3 <----> GPIO2
# B <----> #5 <----> GPIO3
# G <----> #7 <----> GPIO4
R,G,B=2,4,3
# 设置树莓派GPIO操作Python库在操作GPIO时使用BCM编码,即编号为上面所说的GPIO2、GPIO3、GPIO4
RPi.GPIO.setmode(RPi.GPIO.BCM)
# 将RGB三色灯所使用的3个GPIO引脚配置为GPIO输出模式
RPi.GPIO.setup(R, RPi.GPIO.OUT)
RPi.GPIO.setup(G, RPi.GPIO.OUT)
RPi.GPIO.setup(B, RPi.GPIO.OUT)
# 将RGB三色灯所使用的3个GPIO引脚配置为高电平,这样三个Led灯默认都熄灭了。
RPi.GPIO.output(R, RPi.GPIO.HIGH)
RPi.GPIO.output(G, RPi.GPIO.HIGH)
RPi.GPIO.output(B, RPi.GPIO.HIGH)
# 开始循环控制3个颜色的灯亮/灭,其中低电平点亮,高电平灭掉。
try:
t = 0.5
while True:
RPi.GPIO.output(R, RPi.GPIO.LOW)
time.sleep(t)
RPi.GPIO.output(R, RPi.GPIO.HIGH)
time.sleep(t)
RPi.GPIO.output(G, RPi.GPIO.LOW)
time.sleep(t)
RPi.GPIO.output(G, RPi.GPIO.HIGH)
time.sleep(t)
RPi.GPIO.output(B, RPi.GPIO.LOW)
time.sleep(t)
RPi.GPIO.output(B, RPi.GPIO.HIGH)
time.sleep(t)
except KeyboardInterrupt:
pass
# 退出时记得清理一下Python的GPIO库
RPi.GPIO.cleanup()
接下来我们执行该Python脚本,注意需要sudo 权限才能控制树莓派的硬件GPIO。
pi@raspberrypi:~ $ chmod +x led.py
pi@raspberrypi:~ $ vim led.py
此时,就可以看到RGB三色Led灯开始轮流亮灭了。
2.2 Linuxt标准GPIO接口
早期的Linux系统,我们如果想要通过编程控制最底层的硬件的话,则必须要在Linux内核里编写相应设备的驱动,通过Linux系统调用导出相应的接口函数给应用程序调用,这样才能控制最底层的硬件。即使是一个非常简单的需求,如需要使用GPIO控制Led灯亮灭也要写一个Led设备驱动才能实现,这是因为只有Linux内核才有控制硬件的权限,上层的应用程序是不能直接操作硬件的。
随着Linux内核的不断发展,在应用程序空间能够直接操作底层GPIO硬件的需求也不断增加,但并不是所有的程序员都具备Linux设备驱动开发的能力,毕竟这还是有一定技术壁垒的技术活,需要很多硬件知识以及Linux内核设备驱动开发的相关知识。
为了满足这种日益增长的需求,Linux内核对整个GPIO子系统进行了重构,由各个CPU厂商(如NXP、ATMEL、ST等公司)编写自己不同的 GPIO 控制子系统 pinctrl,并对上导出统一的接口函数,然后Linux内核使用统一的 GPIOlib接口屏蔽不同的厂商差异,最后为应用程序空间提供统一的 GPIO 操作API。这样,不管你是使用的哪个厂商、那种架构的处理器,在上层应用程序编程时,都会提供统一的编程API。
在上面的GPIOlib 示意图中我们可以看出,Linux内核在应用程序空间导出了两种接口:
2.2.1 sysfs文件接口
2.2.1.1 sysfs接口介绍
为方便Linux用户能够在应用程序空间直接使用命令行来操作控制GPIO,Linux内核 GPIOlib 使用 /sys伪文件系统 导出了一个文件夹 /sys/class/gpio/, 这样用户在命令行通过 echo 命令就可以直接操作这些GPIO口了。接下来我们以控制RGB三色灯的红灯为例,讲解 sysfs接口的 GPIO操作。
pi@raspberrypi:~ $ ls /sys/class/gpio/
export gpiochip0 gpiochip100 gpiochip504 unexport
从前面的分析我们知道,RGB三色灯的红灯连到了树莓派的物理引脚3#引脚上,该引脚为 BCM编码的GPIO2。使用下面命令我们可以导出 GPIO2 的相应接口。
pi@raspberrypi:~ $ sudo sh -c 'echo 2 > /sys/class/gpio/export'
成功执行上面命令后,我们可以看到 /sys/class/gpio/ 路径下多了一个 gpio2 的文件夹。
pi@raspberrypi:~ $ ls /sys/class/gpio/
export gpio2 gpiochip0 gpiochip100 gpiochip504 unexport
接下来我们看看 /sys/class/gpio/gpio2/ 下有哪些文件,其中与我们 GPIO 操作相关的文件有两个: direction 和 value 。
pi@raspberrypi:~ $ ls /sys/class/gpio/gpio2/
active_low device direction edge power subsystem uevent value
其中 direction 用来设置 GPIO的方向,如果设置为 out 则设置GPIO的模式为 output,而设置为 in 则设置GPIO的模式为 input。因为这里我们要通过GPIO控制Led灯的亮灭,所以这里应该设置为 out。
pi@raspberrypi:~ $ sudo sh -c 'echo out > /sys/class/gpio/gpio2/direction'
如果设置GPIO为output模式,我们往 value 文件中写 0 就输出低电平,而写 **1 **就输出高电平。
如果设置GPIO为input模式,我们使用 ***cat
***命令从 value 文件中读到0就是低电平,读到1就是高电平。
因为我们的红灯是低电平亮,高电平灭,所以我们往 value 文件里写 0 红灯就亮了。
pi@raspberrypi:~ $ sudo sh -c 'echo 0 > /sys/class/gpio/gpio2/value'
如果我们使用完了,不想再使用该GPIO口,则可以使用下面命令关闭 GPIO2 的接口。此时 /sys/class/gpio/gpio2/ 接口就消失了。
pi@raspberrypi:~ $ sudo sh -c 'echo 2 > /sys/class/gpio/unexport'
pi@raspberrypi:~ $ ls /sys/class/gpio/
export gpiochip0 gpiochip100 gpiochip504 unexport
2.2.1.2 Shell编程控制
接下来我们编写Shell测试脚本如下,用来测试三色Led是否能够正常工作。
pi@raspberrypi:~ $ vim led.sh
#!/bin/bash
# RGB LED <----> PhyPin <----> BCM Pin
#
# V <----> #1 <----> 3.3V
# R <----> #3 <----> GPIO2
# B <----> #5 <----> GPIO3
# G <----> #7 <----> GPIO4
# GPIO sysfs path
SYS_PATH=/sys/class/gpio/
# Led GPIO pins
LED_R=2
LED_G=4
LED_B=3
# Blink sleep time
BLINK_T=0.5
# set shell script exit when get unset variable or command get error
set -u
set -e
function pr_error()
{
echo -e "\033[40;31m $1 \033[0m"
}
function pr_info()
{
echo -e "\033[40;33m $1 \033[0m"
}
# turn led on/off API function
function turn_led()
{
if [ $# != 2 ] ; then
return ;
fi
led_pin=$1
led_status=$2
gpio_path=$SYS_PATH/gpio${led_pin}
# export the LED GPIO port
if [ ! -d $gpio_path ] ; then
echo $led_pin > $SYS_PATH/export
fi
# set GPIO direction
echo out > $gpio_path/direction
# set Led on/off
if [ $led_status == on ] ; then
echo 0 > $gpio_path/value
else
echo 1 > $gpio_path/value
fi
# unexport the LED GPIO port
echo $led_pin > $SYS_PATH/unexport
}
# blink led API function
function blink_led()
{
if [ $# != 1 ] ; then
return ;
fi
led_pin=$1
turn_led $led_pin on
sleep $BLINK_T
turn_led $led_pin off
sleep $BLINK_T
}
# shell script exit callback function
function exit_handler()
{
turn_led $LED_R off
turn_led $LED_G off
turn_led $LED_B off
}
# check this shell script run as root or not
if [ `id -u` != 0 ] ; then
pr_error "ERROR: This shell script must run as root"
exit;
fi
# set callback function exit_handler() when shell script exit
trap 'exit_handler' EXIT
# infinite loop to blink RGB led
while [ 1 == 1 ] ; do
pr_info "blink red led"
blink_led $LED_R
pr_info "blink green led"
blink_led $LED_G
pr_info "blink blue led"
blink_led $LED_B
done
pi@raspberrypi:~ $ chmod a+x led.sh
pi@raspberrypi:~ $ sudo ./led.sh
blink red led
blink green led
blink blue led
2.2.2 字符设备接口
2.2.2.1 libgpiod介绍
GPIOlib 为Linux应用程序空间提供了一些 GPIO 操作的字符设备 /dev/gpiochipN(N=0,1,2…) , 我们使用该设备驱动提供的编程API,就可以实现相应的GPIO控制。
pi@raspberrypi:~ $ ls -l /dev/gpiochip*
crw-rw---- 1 root gpio 254, 0 Jun 25 11:17 /dev/gpiochip0
crw-rw---- 1 root gpio 254, 1 Jun 25 11:17 /dev/gpiochip1
crw-rw---- 1 root gpio 254, 2 Jun 25 11:17 /dev/gpiochip2
从 linux 4.8 后,官方不再推荐使用 GPIO sysfs 接口。libgpiod 是用于与linux GPIO交互的C库和工具,它封装了 ioctl 调用和简单的API接口。与sysfs方式相比,libgpiod可以保证所有分配的资源,在关闭文件描述符后得到完全释放,并且拥有sysfs方式接口中不存在的功能(如时间轮询,一次设置/读取多个gpio值)。此外libgpiod还包含一组命令行工具,允许用户使用脚本对gpio进行个性化操作。
在树莓派下,我们可以直接使用 apt install -y gpiod libgpiod-dev
命令安装 libgpiod 提供的命令行工具和开发库文件。
pi@raspberrypi:~ $ sudo apt install -y gpiod libgpiod-dev
目前有六个命令行工具可用:
gpiodetect - 列出系统上存在的所有 gpiochips,它们的名称、标签和 GPIO管脚数量。
gpioinfo - 列出指定 gpio 的所属chip、它们的名称、被使用者名字、方向、激活状态和附加标志。
gpioget - 读取指定 GPIO 的值
gpioset - 设置指定 GPIO 的值
gpiofind - 获取 gpiochip 名称和给定行名称的行偏移
gpiomon - 监听 GPIO 上的特定事件
使用 gpiodetect
命令可以查看当前 CPU 所提供的GPIO字符设备数及每一个GPIO设备所管理的GPIO引脚数。
pi@raspberrypi:~ $ sudo gpiodetect
gpiochip0 [pinctrl-bcm2835] (54 lines)
gpiochip1 [brcmvirt-gpio] (2 lines)
gpiochip2 [raspberrypi-exp-gpio] (8 lines)
使用 gpioinfo
命令可以查看当前 CPU 所提供的GPIO字符设备数及每一个GPIO设备所管理的GPIO详细信息。
pi@raspberrypi:~ $ sudo gpioinfo
gpiochip0 - 54 lines:
line 0: "ID_SDA" unused input active-high
line 1: "ID_SCL" unused input active-high
line 2: "SDA1" unused input active-high
line 3: "SCL1" unused input active-high
line 4: "GPIO_GCLK" unused input active-high
line 5: "GPIO5" unused input active-high
line 6: "GPIO6" unused input active-high
line 7: "SPI_CE1_N" unused input active-high
line 8: "SPI_CE0_N" unused input active-high
line 9: "SPI_MISO" unused input active-high
line 10: "SPI_MOSI" unused input active-high
line 11: "SPI_SCLK" unused input active-high
line 12: "GPIO12" unused input active-high
line 13: "GPIO13" unused input active-high
line 14: "TXD1" unused input active-high
line 15: "RXD1" unused input active-high
line 16: "GPIO16" unused input active-high
line 17: "GPIO17" unused input active-high
line 18: "GPIO18" unused input active-high
line 19: "GPIO19" unused input active-high
line 20: "GPIO20" unused input active-high
line 21: "GPIO21" unused input active-high
line 22: "GPIO22" unused input active-high
line 23: "GPIO23" unused input active-high
line 24: "GPIO24" unused input active-high
line 25: "GPIO25" unused input active-high
line 26: "GPIO26" unused input active-high
line 27: "GPIO27" unused input active-high
line 28: "NC" unused input active-high
line 29: "LAN_RUN_BOOT" unused input active-high
line 30: "CTS0" unused input active-high
line 31: "RTS0" unused input active-high
line 32: "TXD0" unused input active-high
line 33: "RXD0" unused input active-high
line 34: "SD1_CLK" unused input active-high
line 35: "SD1_CMD" unused input active-high
line 36: "SD1_DATA0" unused input active-high
line 37: "SD1_DATA1" unused input active-high
line 38: "SD1_DATA2" unused input active-high
line 39: "SD1_DATA3" unused input active-high
line 40: "PWM0_OUT" unused input active-high
line 41: "PWM1_OUT" unused input active-high
line 42: "ETH_CLK" unused input active-high
line 43: "WIFI_CLK" unused input active-high
line 44: "SDA0" unused input active-high
line 45: "SCL0" unused input active-high
line 46: "SMPS_SCL" unused input active-high
line 47: "SMPS_SDA" unused output active-high
line 48: "SD_CLK_R" unused input active-high
line 49: "SD_CMD_R" unused input active-high
line 50: "SD_DATA0_R" unused input active-high
line 51: "SD_DATA1_R" unused input active-high
line 52: "SD_DATA2_R" unused input active-high
line 53: "SD_DATA3_R" unused input active-high
gpiochip1 - 2 lines:
line 0: unnamed "ACT" output active-high [used]
line 1: unnamed unused input active-high
gpiochip2 - 8 lines:
line 0: "BT_ON" unused output active-high
line 1: "WL_ON" unused input active-high
line 2: "STATUS_LED" unused output active-high
line 3: "LAN_RUN" unused output active-high
line 4: "HDMI_HPD_N" "hpd" input active-low [used]
line 5: "CAM_GPIO0" "cam1_regulator" output active-high [used]
line 6: "CAM_GPIO1" unused output active-high
line 7: "PWR_LOW_N" "PWR" input active-high [used]
从之前的分析我们知道,RGB三色灯三个控制引脚分别接到了 GPIO2、GPIO4、GPIO3上。另外,从上面的命令输出中我们可以看出它们都由 gpiochip0 这个设备管。接下来我们可以使用 gpioset
这个命令来控制这三个灯的亮灭。
如通过下面这三条命令,我们可以把 GPIO2、GPIO3、GPIO4拉成低电平,这样RGB三个灯就都亮了。
pi@raspberrypi:~ $ sudo gpioset gpiochip0 2=0
pi@raspberrypi:~ $ sudo gpioset gpiochip0 3=0
pi@raspberrypi:~ $ sudo gpioset gpiochip0 4=0
当然,我们也可以使用下面命令同时控制这三个GPIO引脚。
pi@raspberrypi:~ $ sudo gpioset gpiochip0 2=1 3=1 4=1
2.2.2.2 libgpiod编程
libgpiod 结构体
代表支持的gpio芯片的相关信息
struct gpiod_chip {
struct gpiod_line **lines; //每个gpio芯片的gpiod_line 数组地址,每一个gpio口对应一个line
unsigned int num_lines; //该gpio芯片下的gpio线路数量
int fd; //设备描述符,即库中底层使用ioctl打开的gpio芯片设备节点的描述符
char name[32]; //芯片的名称
char label[32]; //芯片的标签
};
每个gpio芯片下的gpio口的信息
struct gpiod_line {
unsigned int offset; //gpio 的偏移量,如GPIO05_IO09 偏移 9
int direction; //gpio的方向
bool active_low; //是否是低电平有效,前面介绍过此属性
int output_value; //最后写入 GPIO 的逻辑值
__u32 info_flags;
__u32 req_flags;
int state; //和事件相关的一个状态值
struct gpiod_chip *chip; //所属芯片的地址
struct line_fd_handle *fd_handle;
char name[32]; //名字,编程时候可以给使用的gpio赋予名字
char consumer[32]; //使用者名字
};
获取gpio芯片函数
struct gpiod_chip* gpiod_chip_open_by_name(const(char)*name)
功能描述: 打开 gpiochip
参数说明: name:要打开的 gpiochip 的名称
返回值: 成功返回 gpiod_chip 句柄,失败则返回 NULL
GPIO输出操作函数
int gpiod_line_request_output(struct gpiod_line* line,const(char)*consumer,int default_val)
功能描述: 设置GPIO口为GPIO输出模式
参数说明: line: GPIO口相对应的 gpiod_line 句柄
consumer:使用者的名称
default_val:GPIO口的默认状态值
返回值: 成功返回0,失败则返回-1
int gpiod_line_set_value(struct gpiod_line* line,int value)
功能描述: 设置 GPIO 输出口的电平状态
参数说明: line: GPIO口相对应的 gpiod_line 句柄
value:要设置的GPIO口状态值
返回值: 成功返回0,失败则返回-1
GPIO输入操作函数
int gpiod_line_request_input(struct gpiod_line *line, const char *consumer);
功能描述: 设置GPIO口为GPIO输入模式
参数说明: line: GPIO口相对应的 gpiod_line 句柄
consumer:使用者的名称
返回值: 成功返回0,失败则返回-1
int gpiod_line_get_value(struct gpiod_line *line);
功能描述: 获取 GPIO 输入口的电平状态
参数说明: line: GPIO口相对应的 gpiod_line 句柄
返回值: 成功返回0或1(即逻辑值),失败则返回-1
GPIO口释放函数
void gpiod_line_release(struct gpiod_line* line)
功能描述: 释放先前申请的GPIO口所对应的 gpiod_line 句柄
参数说明: line:相应的gpiod_line 句柄
void gpiod_chip_close(struct gpiod_chip* chip)
功能描述: 关闭 gpiod_chip 句柄并释放所有分配的资源
参数说明: chip:相应 gpiod_chip 句柄
2.2.2.3 C语言编程控制
接下来我们编写C程序源码如下,用来学习 libgpiod的函数API 。
pi@raspberrypi:~ $ vim led.c
/*********************************************************************************
* Copyright: (C) 2023 LingYun IoT System Studio
* All rights reserved.
*
* Filename: led.c
* Description: This file is RGB led blink program based on libgpiod
*
* Version: 1.0.0(01/07/23)
* Author: Guo Wenxue <guowenxue@gmail.com>
* ChangeLog: 1, Release initial version on "01/07/23 01:39:48"
*
********************************************************************************/
#include <gpiod.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
/*
RGB LED <----> PhyPin <----> BCM Pin
V <----> #1 <----> 3.3V
R <----> #3 <----> GPIO2
B <----> #5 <----> GPIO3
G <----> #7 <----> GPIO4
*/
/* RGB blink sleep time */
#define BLINK_T 1
/* RGB Led index */
enum
{
LED_R,
LED_G,
LED_B,
LED_MAX,
};
/* RGB Led status */
enum
{
OFF=0,
ON,
};
/* RGB led struct */
typedef struct led_gpio_s
{
int idx; /* led index */
int gpio; /* led GPIO port */
const char *desc; /* led description */
struct gpiod_line *line; /* led gpiod line */
} led_gpio_t;
/* RGB led information */
led_gpio_t leds[LED_MAX] =
{
{ LED_R, 2, "red", NULL},
{ LED_G, 4, "green", NULL},
{ LED_B, 3, "blue", NULL},
};
int g_stop = 0;
void sig_handler(int signum)
{
switch(signum)
{
case SIGINT:
case SIGTERM:
g_stop = 1;
default:
break;
}
return ;
}
/* turn RGB led on/off API function */
void turn_led(int which, int status)
{
if( which<0 || which >= LED_MAX )
return ;
printf("turn %5s led GPIO#%d %s\n", leds[which].desc, leds[which].gpio, status?"on":"off");
if( OFF == status )
gpiod_line_set_value(leds[which].line, 1);
else
gpiod_line_set_value(leds[which].line, 0);
}
/* blink RGB led API function */
void blink_led(int which)
{
if( which<0 || which >= LED_MAX )
return ;
turn_led(which, ON);
sleep(BLINK_T);
turn_led(which, OFF);
sleep(BLINK_T);
}
int main(int argc, char **argv)
{
const char *chipname = "gpiochip0";
struct gpiod_chip *chip;
int i;
/* open GPIO chip */
chip = gpiod_chip_open_by_name(chipname);
if( !chip )
{
printf("gpiod open '%s' failed: %s\n", chipname, strerror(errno));
return 1;
}
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
for(i=0; i<LED_MAX; i++)
{
/* open LED GPIO lines */
leds[i].line = gpiod_chip_get_line(chip, leds[i].gpio);
/* set LED lines to output and high level(off) */
gpiod_line_request_output(leds[i].line, "rgbled", 1);
}
while( !g_stop )
{
blink_led(LED_R);
blink_led(LED_G);
blink_led(LED_B);
}
/* turn off leds and release lines */
for(i=0; i<LED_MAX; i++)
{
turn_led(i, OFF);
gpiod_line_release(leds[i].line);
}
/* release chip */
gpiod_chip_close(chip);
return 0;
}
接下来我们编译C代码并运行可执行程序,需要注意的是在编译时要指定链接 libgpiod 库文件。
pi@raspberrypi:~ $ gcc led.c -lgpiod -o led
pi@raspberrypi:~ $ sudo ./led
turn red led GPIO#2 on
turn red led GPIO#2 off
turn green led GPIO#4 on
turn green led GPIO#4 off
turn blue led GPIO#3 on
turn blue led GPIO#3 off
3. 树莓派PWM接口
https://wiki.st.com/stm32mpu/wiki/PWM_overview
https://github.com/dotnet/iot/blob/main/Documentation/raspi-pwm.md
3.1 PWM 介绍及参数
PWM(Pulse Width Modulation),是脉冲宽度调制缩写,它是通过对一系列脉冲的宽度进行调制,等效出所需要的波形(包含形状以及幅值),对模拟信号电平进行数字编码,也就是说通过调节占空比的变化来调节信号、能量等的变化,占空比就是指在一个周期内,信号处于高电平的时间占据整个信号周期的百分比,例如方波的占空比就是50%。PWM是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。PWM信号一般下图所示:
![](images/pwm_duty.jpg)
在聊到PWM时,我们必定会提到如下几个参数:
PWM的频率: 是指在1秒钟内,信号从高电平到低电平再回到高电平的次数,也就是说一秒钟PWM有多少个周期,单位Hz。
PWM的周期: T=1/f,T是周期,f是频率。如果频率为50Hz ,也就是说一个周期是20ms,那么一秒钟就有 50次PWM周期。
占空比: 脉宽除以脉冲周期的值,百分数表示,比如50%。也常有小数或分数表示的,比如0.5或1/2。 也就是是一个脉冲周期内,高电平的时间与整个周期时间的比例,如下图所示。
3.2 PWM应用
PWM就是脉冲宽度调制,通过调节占空比就可以调节脉冲宽度。利用PWM的这个特性,我们在日常生活或工业控制中有非常广泛的应用。
3.2.1 呼吸灯
一般人眼睛对于80Hz以上刷新频率则完全没有闪烁感,那么我们平时见到的LED灯,当它的频率大于50Hz的时候,人眼就会产生视觉暂留效果,基本就看不到闪烁了,而是误以为是一个常亮的LED灯。 由于频率很高时看不到闪烁,占空比越大LED越亮,占空比越小LED越暗。所以,在频率一定时,可以用不同占空比改变LED灯的亮度,从而达到一个呼吸灯的效果。
![](images/pwm_led.gif)
3.2.2 蜂鸣器
3.2.3 舵机控制
舵机的控制就是通过一个固定的频率,给其不同的占空比来控制舵机不同的转角。 舵机的频率一般为频率为50HZ,也就是一个20ms左右的时基脉冲,而脉冲的高电平部分一般为0.5ms-2.5ms范围,来控制舵机不同的转角。 500-2500us的PWM高电平部分对应控制180度舵机的0-180度。 以180度角度伺服为例,那么对应的控制关系是这样的:
0.5ms————-0度;
1.0ms————-45度;
1.5ms————-90度;
2.0ms————-135度;
2.5ms————-180度;
下图演示占空比从1ms变化到2ms时,转角的变化。
3.2.4 直流电机
对于直流电机来讲,电机输出端引脚是高电平电机就可以转动,当输出端高电平时,电机会转动,但是是一点一点的提速,在高电平突然转向低电平时,电机由于电感有防止电流突变的作用是不会停止的,会保持这原有的转速,以此往复,电机的转速就是周期内输出的平均电压值,所以实质上我们调速是将电机处于一种,似停非停,似全速转动又非全速转动的状态,那么在一个周期的平均速度就是我们占空比调出来的速度了。
在电机控制中,电压越大,电机转速越快,而通过PWM输出不同的模拟电压,便可以使电机达到不同的输出转速。 当然,在电机控制中,不同的电机都有其适应的频率。频率太低会导致运动不稳定,如果频率刚好在人耳听觉范围(20Hz-20kHz),有时还会听到呼啸声。频率太高时,电机可能反应不过来,正常的电机频率在 6-16kHZ之间为好。
3.3 Linux标准PWM接口
3.3.1 硬件连接分析
2.5.3.1 驱动配置使用说明
查看开发板底板原理图和其40pin扩展口可以知道,开发板上可以使用的有4路PWM,其中
PWM1 ---> backlight //LCD背光
PWM2 ---> beep //蜂鸣器
PWM7,PWM8 ---> 40pin扩展 //需要使能开启
想要使能40pin扩展口的PWM7和8的话,需要修改开发板上的DTOverlay配置文件,添加两个管脚的PWM支持。
root@igkboard:~# vi /run/media/mmcblk1p1/config.txt
# Enable PWM overlays, PWM8 conflict with UART8(NB-IoT/4G module)
dtoverlay_pwm=7 8
修改完成后重启系统,和之前sysfs操控gpio的方式一样,PWM 同样也是通过 sysfs 方式进行操控,进入到/sys/class/pwm 目录下,可以看到四个以pwmchipX(X表示数字0~3)命名的文件夹,这4个文件夹其实就对应了I.MX6U的4个PWM控制器,,其实I.MX6U总共有8个PWM控制器,大家可以通过查询I.MX6U参考手册得知。由于我们只使能了四个,所以只看到四个控制器文件夹。
root@igkboard:/sys/class/pwm# ls
pwmchip0 pwmchip1 pwmchip2 pwmchip3
易知其对应方式是如下所示
pwmchip0 ---> pwm1
pwmchip1 ---> pwm2
pwmchip2 ---> pwm7
pwmchip3 ---> pwm8
2.5.3.2 pwmchip属性简介
由于 pwm1 被LCD背光驱动占用,我们以另外三个中的一个作为示例测试,进入pwmchip1(pwm2 物理连接板载蜂鸣器)文件夹下
root@igkboard:/sys/class/pwm# cd pwmchip1/
root@igkboard:/sys/class/pwm/pwmchip1# ls
consumers device export npwm power subsystem suppliers uevent unexport
只需要重点关注三个属性文件,export、npwm以及unexport ,下面一一介绍:
npwm:这是一个只读属性,读取该文件可以得知该PWM控制器下共有几路PWM输出,如下所示:
root@igkboard:/sys/class/pwm/pwmchip1# cat npwm
1
export:与GPIO控制一样,在使用PWM之前,也需要将其导出,通过export属性进行导出,以下所示:
root@igkboard:/sys/class/pwm/pwmchip1# echo 0 > export
root@igkboard:/sys/class/pwm/pwmchip1# ls
consumers device export npwm power pwm0 subsystem suppliers uevent unexport
导出之后,pwmchip1文件夹下生成一个名为pwm0的文件夹,稍后介绍。注意导出的编号(echo 0)必须小于npwm(1)的值
unexport:将导出的PWM删除。当使用完PWM之后,我们需要将导出的PWM删除,如下所示
root@igkboard:/sys/class/pwm/pwmchip1# echo 0 > unexport
root@igkboard:/sys/class/pwm/pwmchip1# ls
consumers device export npwm power subsystem suppliers uevent unexport
写入到unexport文件中的编号与写入到export文件中的编号是相对应的;需要注意的是,export文件和unexport文件都是只写的、没有读权限。
2.5.3.3 如何控制PWM
上文成功在pwmchip1中导出pwm0文件夹后,我们进入pwm0文件夹下后看看:
root@igkboard:/sys/class/pwm/pwmchip1/pwm0# ls
capture consumers duty_cycle enable period polarity power suppliers uevent
该目录下也有一些属性文件,我们重点关注duty_cycle、enable、period以及polarity这四个属性文件,接下来一一进行介绍。
enable:可读可写,写入”0”表示禁止PWM;写入”1”表示使能PWM。读取该文件获取PWM当前是禁止还是使能状态。
echo 0 > enable #禁止PWM输出
echo 1 > enable #使能PWM输出
polarity:用于设置极性,可读可写,可写入的值如下。
echo normal > polarity #默认极性
echo inversed > polarity #极性反转
很多SoC的PWM外设其硬件上并不支持极性配置,所以对应的驱动程序中并未实现这个接口,应用层自然也就无法通过polarity属性文件对PWM极性进行配置,IGKBoard开发板系统便是如此。
period:用于配置PWM周期,可读可写;写入一个字符串数字值,以**ns(纳秒)**为单位,譬如配置PWM周期为10us(微秒)。
echo 10000 > period #PWM周期设置为10us(10 * 1000ns)
duty_cycle:用于配置PWM的占空比,可读可写;写入一个字符串数字值,同样也是以ns为单位.
echo 5000 > duty_cycle #PWM占空比设置为5us
4. 树莓派一线协议
5. 树莓派I2C接口
5.1 I2C协议简介
在消费电子和工业电子等领域中,会使用各种类型的芯片,有时需要快速地进行数据交互,为了使用最简单的方式使这些芯片互联互通,于是I2C诞生了。I2C(Inter-Integrated Circuit)是一种通用的总线协议。它是Philips公司半导体事业部(现在的NXP)在80年代初为方便主板、嵌入式系统设计的一种简单、双向二线制同步串行总线。由于其简单性,它被广泛用于微控制器与传感器阵列、显示器、IoT设备、EEPROM等之间的通信。I2C的专利在2006年11月1日已到期,大家可以免费使用。
对于硬件工程师来说,只需要2个管脚,极少的连接线和面积,就可以实现芯片间的通信,对于软件设计人员来说,可以使用同一个I2C驱动库,来实现不同器件的驱动,大大减少了软件的开发时间。极低的工作电流,降低了系统的功耗,完善的应答机制大大增强了通信的可靠性。
5.1.1 I2C物理层
I2C是一钟支持多器件的主从模式、同步、串行、全双工通信的总线,它只需要两根线就能够实现总线上所有设备的通信:
SCL(Serial Clock,串行时钟) : 该线上主要用来发送通信的时钟信号;
SDA(Serial Data,串行数据) : 该线上主要用来发送通信的数据信号;
I2C通讯设备之间的常用连接方式如下图
I2C 总线具有如下特性:
SDA和SCL都是双向线路,通常都通过一个上拉电阻连接到正的电源电压(如上图),当总线空闲时,两条线路上默认都是高电平。连接到总线的器件输出级必须是漏极开路或集电极开路才能执行线与的功能;
I2C 总线是一种主从结构(Master/Slave)总线,I2C 总线上的每个设备都可以作为主设备或者从设备, 但通常一个总线上只有1个主设备以及1个或多个从设备。当然I2C总线也支持多个通信主机及多个通信从机,在该模式下为了防止数据冲突,需利用仲裁方式决定由哪个器件占用总线;
主设备(通常是CPU)将用来产生允许传输的时钟信号,并初始化总线的数据传输;而从设备(如传感器芯片、EEPROM等)只能被动响应主设备发起的通信请求。每个连接到总线的从设备都有一个7-bit设备地址(通常在芯片datasheet里可以找到),主机可以利用这个地址进行不同设备之间的访问。
I2C总线具有三种传输模式:标准模式传输速率为100kbit/s ,快速模式为400kbit/s , 高速模式下可达 3.4Mbit/s,但目前大多I2C设备尚不支持高速模式。
连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制;
下图显示了I2C总线上主设备与多个从设备通信的大致过程。
5.1.2 I2C协议层
I2C总线上的所有数据都是以8位字节传送的,发送器每发送一个字节,就在第9个时钟脉冲期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
在 I2C 总线上传送的每一位数据都由一个同步时钟脉冲相对应,即在 SCL 串行时钟的配合下,数据在 SDA 上从高位向低位依次串行传送每一位的数据。下面是 I2C 通信的时序图:
5.1.2.1 起始位
I2C 总线在空闲时 SDA 和 SCL 都处于高电平状态(由上拉电阻拉成高电平),当主设备要开始一次 I2C 通信时就发送一个 START(S)信号,这个起始位就可以告诉所有 I2C 从机,“我”要开始进行 I2C 通信了;当要结束一次 I2C 通信时,则发送一个 STOP(P)信号结束本次通信。
I2C 协议起始位:当 SCL 保持高电平时,SDA 出现一个下降沿,产生一个起始位;
I2C 协议停止位:当 SCL 保持高电平时,SDA 出现一个上升沿,产生一个停止位;
5.1.2.2 I2C读写地址
主机在发送 START 信号之后,第 2 个时序应该立刻给出要通信的目标从机物理地址。此外,I2C 总线是一种能够实现半双工通信的同步串行通信协议,站在主设备的角度来看应该具有读/写从设备的功能。
这时候 I2C 的读写地址除了 7bit 物理地址以外,还有 1bit 用来标识读/写方向位。这样I2C 的从设备读写地址通常是一个字节,其中高 7bit 是上面描述的物理地址,最低位用来表示读写方向(0 为写操作,1 为读操作)。
5.1.2.3 I2C应答信号
主机往 I2C 总线上传输器件地址,所有的从机接收到这个地址后与自己的地址相比较若相同则发出一个应答 ACK 信号,主机收到这个应答信号后通讯连接建立成功,若未收到应答信号则表示寻址失败。可以在上文中总线时序中第9个时钟时的电平高低进行判断。
此外,主/从机在之后的数据通信中,数据接收方(可能是主机也可能是从机)收到传输的一个字节数据后,需要给出响应,此时处在第九个时钟,发送端释放 SDA 线控制权,将 SDA 电平拉高,由接收方控制。
若希望继续,则给出“应答(ACK, Acknowledge)”信号,即SDA 为低电平;
若不希望继续,则给出“非应答(NACK,Not Acknowledge)”信号,即 SDA 为高电平;
5.1.2.4 数据位收发
主机在收到从机的应答信号之后,开始给从机发送数据。SDA 数据线上的每个字节必须是 8 位,每次传输的字节数量没有限制,每个字节发送完成之后,从机必须跟一个应答信号。
I2C 总线通信时数据位传输采用 **MSB(最高位优先)**方式发送,其中高电平表示数据位 1,低电平表示数据位 0。当传输的数据位需要改变时(如上一个位发送的是 1,下一个位要发送 0),必须发生在 SCL 为低电平期间。即规定所有设备在SCL高时候,进行数据的获取,SCL低时候,进行数据的变化。
5.1.2.5 总线速率
I2C 总线是一种同步、半双工、采用电平信号收发的串行总线,其速率支持:
标准模式(Standard-mode):速率高达 100kbit/s
快速模式(Fast-mode):速率高达 400kbit/s
快速模式+(Fast-mode Plus):速率高达 1Mbit/s。
高速模式(High-speed mode):速率高达 3.4Mbit/s
超快速模式(Ultra Fast-mode):速率高达 5Mbit/s (单向传输时支持)
5.1.2.6 主机发送数据流程
主机在检测到总线为空闲时,发送一个启动信号“S”,开始一次通信的开始;
主机接着发送一个从设备地址,它由 7bit 物理地址和 1bit 读写控制位 R/W 组成(此时 R/W=0 写操作);
相对应的从机收到命令字节后向主机回馈应答信号 ACK(ACK=0);
主机收到从机的应答信号后开始发送第一个字节的数据;
从机收到数据后返回一个应答信号 ACK;
主机收到应答信号后再发送下一个数据字节;
主机发完最后一个字节并收到 ACK 后,向从机发送一个停止信号 P 结束本次通信并释放总线;
从机收到 P 信号后也退出与主机之间的通信;
5.1.2.7 主机接收数据流程
主机发送启动信号后,接着发送地址字节(其中 R/W=1 读操作);
对应的从机收到地址字节后,返回一个应答信号并向主机发送数据;
主机收到数据后向从机反馈一个应答信号 ACK;
从机收到应答信号后再向主机发送下一个数据;
当主机完成接收数据后,向从机发送一个 NAK,从机收到非应答信号后便停止发送;
主机发送非应答信号后,再发送一个停止信号,释放总线结束通信;
6. 树莓派SPI接口
6.1 SPI接口介绍
6.2 SPI接口使能与测试
6.3 RC522读卡器
6.3.1 硬件连接与配置
如下图所示,使用杜邦线连接 RC522 模块到树莓派40Pin扩展口的相应引脚上。
下面是RC522模块与树莓派的40Pin引脚连接对应表。
RC522模块引脚 |
树莓派40Pin引脚 |
备注说明 |
---|---|---|
3.3V |
#17(3.3V) |
电源接树莓派任意3.3v引脚都行 |
MOSI |
#19(SPI_MOSI) |
|
MISO |
#21(SPI_MOSI) |
|
SCK |
#23(SPI_CLK) |
|
GND |
#20(Ground) |
电源地莓派任意地引脚都行 |
RST |
#22(GPIO25) |
复位引脚 |
SDA |
#24(SPI_CE0) |
SPI片选引脚 |
IRQ |
中断引脚,轮训模式无需连接 |
接下来我们使用 raspi-config
命令使能树莓派的 SPI 相应引脚功能。
guowenxue@raspberrypi:~ $ sudo raspi-config
在弹出的图形化界面中,我们可以使用上、下、左、右、Tab键 和 回车键来切换选择。
上述使能后,可以看到 /boot/config.txt 文件中 spi 选项被打开。其实,我们也可以直接修改该配置选项让其生效。
guowenxue@raspberrypi:~ $ sudo vim /boot/config.txt
dtparam=spi=on
6.3.2 Python代码读卡
这里我们测试所用的树莓派系统为 Raspbian11(bullseye),其默认的 Python 版本为 3.9.2。
guowenxue@raspberrypi:~ $ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description: Raspbian GNU/Linux 11 (bullseye)
Release: 11
Codename: bullseye
guowenxue@raspberrypi:~ $ sudo python --version
Python 3.9.2
首先使用 pip
命令来安装 RC522 的 Python 开发库。
guowenxue@raspberrypi:~ $ sudo pip3 install mfrc522
接下来我们编写 RC522 读取卡信息的 Python 代码如下:
guowenxue@raspberrypi:~ $ vim rc522_read.py
import RPi.GPIO as GPIO
from mfrc522 import SimpleMFRC522
GPIO.setwarnings(False)
reader = SimpleMFRC522()
print("")
print("+----------------------------+")
print("| RC522 Demo Program Running |")
print("+----------------------------+")
print("")
print("Wait for reading M1 card...")
print("")
try:
while True:
id, text = reader.read()
print(id, text)
except KeyboardInterrupt:
GPIO.cleanup() # Clean up GPIO pins when Ctrl+C is pressed
接下来开始使用 RC522 读卡,当卡靠近 RC522 模块时,就可以读到卡 ID 了。
guowenxue@raspberrypi:~ $ python rc522_read.py
+----------------------------+
| RC522 Demo Program Running |
+----------------------------+
Wait for reading M1 card...
207929235597
207929235597
207929235597
207929235597
207929235597
207929235597
207929235597
下面是卡靠近读卡器时程序运行的结果。
6.3.3 Python代码写卡
接下来我们编写 RC522 写卡信息的 Python 代码如下:
guowenxue@raspberrypi:~ $ vim rc522_write.py
import RPi.GPIO as GPIO
from mfrc522 import SimpleMFRC522
GPIO.setwarnings(False)
reader = SimpleMFRC522()
print("")
print("+----------------------------+")
print("| RC522 Demo Program Running |")
print("+----------------------------+")
print("")
try:
text = input('Please contact card and input new data: ')
reader.write(text)
print("Card write success!")
finally:
GPIO.cleanup()
下面是写卡后再重新读卡内容的运行的结果。