版权声明

本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。

  • Copyright (C) 2023 凌云物网智科实验室·郭工

  • Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292

wechat_pub

树莓派硬件开发篇

树莓派开发板之所以如此受欢迎,并且也称为很多极客的玩物,甚至很多公司都会直接用树莓派做产品,或设计类似的产品,最主要的原因是它提供了一组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系统硬件开发时,其实买哪个版本的树莓派区别不大,这个可以根据自己的实际预算来选择。

rpi_40pinout

在新版本的Raspbian系统中添加了查看树莓派40Pin引脚定义的 pinout 命令,如果没有安装的话,可以使用下面命令安装。

pi@raspberrypi:~ $ sudo apt install -y python3-gpiozero

通过该命令我们可以在命令行中查看相应的引脚定义,如下图所示:

cmd_pinout

关于树莓派的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,当然大家也可以使用默认的 vinano 编辑器替代。如果默认没有安装 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)。

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 里的 PREFIXCC 变量,如果 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

需要注意的是,通常RGB三色Led灯硬件设计上有两种接法,如下图所示:

rgb_sch

  • 共阳极 将所有发光二极管的阳极(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) 引脚。

rpi_rgbled

从上面的连接示意图我们可以看出:

  • 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灯开始轮流亮灭了。

rgbled_on

2.2 Linuxt标准GPIO接口

早期的Linux系统,我们如果想要通过编程控制最底层的硬件的话,则必须要在Linux内核里编写相应设备的驱动,通过Linux系统调用导出相应的接口函数给应用程序调用,这样才能控制最底层的硬件。即使是一个非常简单的需求,如需要使用GPIO控制Led灯亮灭也要写一个Led设备驱动才能实现,这是因为只有Linux内核才有控制硬件的权限,上层的应用程序是不能直接操作硬件的。

Linux_driver_overview

随着Linux内核的不断发展,在应用程序空间能够直接操作底层GPIO硬件的需求也不断增加,但并不是所有的程序员都具备Linux设备驱动开发的能力,毕竟这还是有一定技术壁垒的技术活,需要很多硬件知识以及Linux内核设备驱动开发的相关知识。

为了满足这种日益增长的需求,Linux内核对整个GPIO子系统进行了重构,由各个CPU厂商(如NXP、ATMEL、ST等公司)编写自己不同的 GPIO 控制子系统 pinctrl,并对上导出统一的接口函数,然后Linux内核使用统一的 GPIOlib接口屏蔽不同的厂商差异,最后为应用程序空间提供统一的 GPIO 操作API。这样,不管你是使用的哪个厂商、那种架构的处理器,在上层应用程序编程时,都会提供统一的编程API。

gpiolib_overview

在上面的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 操作相关的文件有两个: directionvalue

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信号一般下图所示:

在聊到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灯的亮度,从而达到一个呼吸灯的效果。

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时,转角的变化。

img

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_Master_Slaver_connect

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总线上主设备与多个从设备通信的大致过程。

i2c

5.1.2 I2C协议层

I2C总线上的所有数据都是以8位字节传送的,发送器每发送一个字节,就在第9个时钟脉冲期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。

在 I2C 总线上传送的每一位数据都由一个同步时钟脉冲相对应,即在 SCL 串行时钟的配合下,数据在 SDA 上从高位向低位依次串行传送每一位的数据。下面是 I2C 通信的时序图:

I2C_Communication_sequence

5.1.2.1 起始位

I2C 总线在空闲时 SDASCL 都处于高电平状态(由上拉电阻拉成高电平),当主设备要开始一次 I2C 通信时就发送一个 START(S)信号,这个起始位就可以告诉所有 I2C 从机,“我”要开始进行 I2C 通信了;当要结束一次 I2C 通信时,则发送一个 STOP(P)信号结束本次通信。

I2C_Start_Stop_signal

  • I2C 协议起始位:当 SCL 保持高电平时,SDA 出现一个下降沿,产生一个起始位;

  • I2C 协议停止位:当 SCL 保持高电平时,SDA 出现一个上升沿,产生一个停止位;

5.1.2.2 I2C读写地址

主机在发送 START 信号之后,第 2 个时序应该立刻给出要通信的目标从机物理地址。此外,I2C 总线是一种能够实现半双工通信的同步串行通信协议,站在主设备的角度来看应该具有读/写从设备的功能。

这时候 I2C 的读写地址除了 7bit 物理地址以外,还有 1bit 用来标识读/写方向位。这样I2C 的从设备读写地址通常是一个字节,其中高 7bit 是上面描述的物理地址,最低位用来表示读写方向(0 为写操作,1 为读操作)。

I2C_Read_Write_address

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低时候,进行数据的变化。

I2C_Data_Bit_Data_Bit_Rev_Send

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 主机发送数据流程

  1. 主机在检测到总线为空闲时,发送一个启动信号“S”,开始一次通信的开始;

  2. 主机接着发送一个从设备地址,它由 7bit 物理地址和 1bit 读写控制位 R/W 组成(此时 R/W=0 写操作);

  3. 相对应的从机收到命令字节后向主机回馈应答信号 ACK(ACK=0);

  4. 主机收到从机的应答信号后开始发送第一个字节的数据;

  5. 从机收到数据后返回一个应答信号 ACK

  6. 主机收到应答信号后再发送下一个数据字节;

  7. 主机发完最后一个字节并收到 ACK 后,向从机发送一个停止信号 P 结束本次通信并释放总线;

  8. 从机收到 P 信号后也退出与主机之间的通信;

I2C_Master_Send_Data_Process

5.1.2.7 主机接收数据流程

  1. 主机发送启动信号后,接着发送地址字节(其中 R/W=1 读操作);

  2. 对应的从机收到地址字节后,返回一个应答信号并向主机发送数据;

  3. 主机收到数据后向从机反馈一个应答信号 ACK

  4. 从机收到应答信号后再向主机发送下一个数据;

  5. 当主机完成接收数据后,向从机发送一个 NAK,从机收到非应答信号后便停止发送;

  6. 主机发送非应答信号后,再发送一个停止信号,释放总线结束通信;

I2C_Master_Receive_Data_Process

6. 树莓派SPI接口

6.1 SPI接口介绍

6.2 SPI接口使能与测试

6.3 RC522读卡器

6.3.1 硬件连接与配置

如下图所示,使用杜邦线连接 RC522 模块到树莓派40Pin扩展口的相应引脚上。

rc522_pihat

下面是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键 和 回车键来切换选择。

spi_enable

spi_enable

spi_enable

上述使能后,可以看到 /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 

下面是卡靠近读卡器时程序运行的结果。

rc522_read

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()    

下面是写卡后再重新读卡内容的运行的结果。

rc522_write

7. 树莓派串行接口