嵌入式即嵌入式系统,以应用为中心,以计算机技术为基础,软硬件可剪裁,适应应用系统对功能、可靠性、成本、体积、功耗等严格要求的专用计算机系统
嵌入式芯片:单片机如共享单车的锁
低端单片机搞不定,用ARM架构,如STM32,比如ARM+Linux+QT,比如安卓系统
QT方案:通常基于Linux 安卓方案:基于安卓,高通,华为海思等
英国ARM公司(中国区总部在上海)
硬件架构的一种:ARM结构 stm32,高通,树莓派等
Intel架构 x86架构 等
官网 https://www.raspberrypi.org 下载镜像文件,DiskImager 写入SD卡
方式一:HDMI链接到显示器
默认账号:pi
默认密码:raspberry
方式2:串口
默认情况,树莓派串口与蓝牙链接
断开蓝牙链接,用串口来通信
1.打开SD卡根目录的"config.txt"文件,将以下内容添加在最后并且保存
dtoverlay=pi3-miniuart-bt
这样就停止了蓝牙,解除了对串口的占用。
2.然后再修改根目录的"cmdline.txt",将里面的内容全部替换成以下内容
dwc_otg.lpm_enable=0 console=tty1 console=serial0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait
配置修改完成。
此时因该配置网络,以便采用ssh远程登录
sudo nano /etc/wpa_supplicant/wpa_supplicant.conf
//文档中加入
network={
ssid="WIFINAME"
psk="PASSWORD"
}
为了便于操作,每次可让树莓派都获取相同的IP地址
sudo nano /etc/rc.local
在文件末尾前加入
ifconfig wlan0 192.168.8.105
方式三:网络登录树莓派
树莓派自带ssh服务,需要手动打开
sudo raspi-config
进入Interfacing Options
打开ssh远程登录软件如SecureCRT,并登录
更新vim,为了更新,需要将源改为国内源
打开终端 输入
sudo nano /etc/apt/sources.list
用#注释或直接删除原有的内容,新增两条:
deb http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ stretch main contrib non-free rpi
#deb-src http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ stretch main contrib non-free rpi
ctrl+x 保存并退出。
更新源并安装
sudo apt-get update
sudo apt-get install vim
访问图形界面
接用远程桌面链接来访问图形界面
下载xrdp
sudo apt-get install xrdp
重启xrdp服务
sudo service xrdp restart
文件共享
借助FileZilla来实现文件共享
ip地址选项加上sftp
分模块的编程思想
适用于多人协作和便于调试
将功能性的函数放入几个c文件中,并分别做同名的头文件,主函数使用时引入它们
在自己定义的头文件和c文件目录下,编译时一起将文件名作为参数给gcc,gcc 将把它们编译成一个可执行文件
静态函数库:程序执行前即编译时就加入到目标程序
优点:使用静态库加载速度更快,发布时无需提供
缺点:链接时完整地拷贝到可执行文件中,移植方便,但更新发布麻烦
动态函数库:程序在执行时,调用动态(临时)由目标程序去调用(linux上叫做共享对象库,文件后缀.so,windows上叫做动态加载函数,文件后缀.dll
优点:链接时不复制,运行时加载,多个程序可用,节省内存且更新和优化更方便
缺点:加载速度慢于静态库,发布时要提供依赖的动态库
无论静态库和动态库,原材料:源代码.c/.cpp
如何生成
1.将.c/.cpp生成.o文件
gcc a.c b.c -c
2.将.o打包
ar rcs libname.a file1.o file2.o
#将file1和file2打包为静态库,命名格式libname.a(以lib开头)
如何使用:
在得知头文件和拥有.a文件后
gcc name.c -lname -L./
-l后是指定要用的动态库,格式为name(不含lib)
-L指定gcc编译器从某个路径下去搜索静态库,不指定从 /usr/lib 下寻找
如何运行
直接生成可执行文件,生成的可执行文件不需要任何依赖,可以直接运行
如何制作
gcc -shared -fpic name.c -o libname.so.a.b.c
#将name.c生成libname.so动态库
# -shared:生成动态库
# -fpic:标准,生成位置无关码
# a.b.c:版本号可省略
如何使用:
和静态库一样,在得知头文件和拥有.so文件后
gcc name.c -lname -L./
-l后是指定要用的动态库,格式为name(不含lib)
-L指定gcc编译器从某个路径下去搜索静态库,不指定从 /usr/lib 下寻找
如何运行
不同于静态库,在得知头文件和拥有.so文件后,此时可执行的文件是不能运行的,因为它在运行时被调用
方案1:
sudo cp libname.c /usr/lib/
移动动态库到lib中,因为默认在此文件下寻找 然后可直接运行
方案2:
添加环境变量
#命令行下输入 export:列出环境变量
#添加l临时环境变量(只在当前终端下使用)
export LD_LIBRARY_PATH="/home/pi/Hao/temp"
写一个shell脚本 run.sh
export LD_LIBRARY_PATH="/home/pi/..."
./a.out
编辑后加入可执行权限
chmod +x run.sh
然后运行此脚本
IO口 :input/output
PWM口:电机调速,灯光亮度
串口:通信
IIC SPI IIS 等
一、电源输出引脚
3.3v、5v代表:3.3伏特和5伏特,是输出供电的正极,也就是我们常说的Vcc
GND代表接地和输出供电的负极
特别注意:每个引脚最大输出电流为16毫安(mA),且同一时刻所有引脚的总输出电流不超过51毫安
二、GPIO
GPIO(General Purpose I/O Ports)意思为通用输入/输出端口,通俗地说,就是一些引脚,可以通过它们输出高低电平或者通过它们读入引脚的状态-是高电平或是低电平。GPIO是个比较重要的概念,用户可以通过GPIO口和硬件进行数据交互(如UART),控制硬件工作(如LED、蜂鸣器等),读取硬件的工作状态信号(如中断信号)等。GPIO口的使用非常广泛。掌握了GPIO,差不多相当于掌握了操作硬件的能力。
树莓派有26个GPIO接口,其中有一部分是复用接口。
1、引脚3、5为IC总线复用接口
2、引脚7为(GCLK)全局时钟引脚复用接口
3、引脚19、21、23为SPI总线复用接口
4、引脚8、10为串口复用接口,TX发送,RX接收
5、引脚12、32、33、35为PWM复用接口
三、IC总线
IC是内部整合电路的称呼,是一种串行通讯总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边装置而发展。IC的正确读法为"Inter-Integrated Circuit" 。
SDA:数据线
SCL:时钟线
四、SPI总线
SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,如今越来越多的芯片集成了这种通信协议。
MISO:数据输入
MOSI:数据输出
SCLK:时钟信号
SS:使能信号
五、UART总线
UART是一种通用串行数据总线,用于异步通信。该总线双向通信,可以实现全双工传输和接收。在嵌入式设计中,UART用于主机与辅助设备通信,如汽车音响与外接AP之间的通信,与PC机通信包括与监控调试器和其它器件,如EEPROM通信。
可以理解为计算机的串口。RS232、TTL
RX是接收, TX是发送.
六、PWM脉冲宽度调制
脉冲宽度调制是一种模拟控制方式,其根据相应载荷的变化来调制晶体管基极或MOS管栅极的偏置,来实现晶体管或MOS管导通时间的改变,从而实现开关稳压电源输出的改变。这种方式能使电源的输出电压在工作条件变化时保持恒定,是利用微处理器的数字信号对模拟电路进行控制的一种非常有效的技术。脉冲宽度调制是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在从测量、通信到功率控制与变换的许多领域中。
它是一个树莓派IO控制库,具有丰富的接口,它是第三方库,需自行下载,命令行 gpio -v 查看版本号
它不是标准库,和pthread一样,因此编译记得-lwiringPi
API
#include
//命令行下输入 gpio readall 查看树莓派接口
/*
使用wiringPi时,你必须在执行任何操作前初始化树莓派,否则程序不能正常工作
失败返回-1
*/
int wiringPiSetup (void);
/*
GPIO控制函数
设置pin(wpi)引脚模式(INPUT/OUTPUT)
*/
void pinMode(int pin, int mode);
/*
设置pin引脚的高低电平(HIGH/LOW)
*/
void digitalWrite (int pin, int value);
/*
功能:读取引脚电平状态
参数:pin:要读取的引脚
返回值:HIGH或者LOW
*/
int digitalRead(int pin);
/*
get the time from 1970-1-1
If either tv or tz is NULL, the corresponding structure is not set or returned.
return 0 for success, or -1 for failure (in which case errno is set appropriately)
*/
#include
int gettimeofday(struct timeval *tv, struct timezone *tz);
//The tv argument is a struct timeval (as specified in )
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of DST correction */
};
串口通信属于全双工
硬件模块的设计,需要相互通信,串口通信需要有相同的数据格式(数据位,停止位,奇偶校验位)和波特率
最开始串口为了联网被用于与终端交互,记得将/boot/cmdline.txt改回来
sudo vi cmdline.txt
#删除 console=tty1 console=serial0,115200
#重启使修改的配置文件生效
API
#include
/*
打开串口
device是设备所在目录路径,baud是波特率
成功返回文件描述符,否则-1
*/
int serialOpen (char* device, int baud);
//路径为/dev/ttyAMA0
/*
关闭串口
*/
void serialClose (int fd);
/*
发送读取一个字节
*/
void serialPutchar (int fd, unsigned char c);
int serialGetchar (int fd);
/*
发送一个字符串
*/
void serialPuts (int fd, char* s);
/*
发送格式化字符串,像printf一样
*/
void serialPrintf(int fd, char* message, ...);
/*
获取串口缓存的字节数
*/
int serialDataAvail(int fd);
/*
清空缓存
*/
void serialFlush (int fd);
//对于发送读取较多内容,可用Linux下标准IO函数read/write
交叉编译:是在一个平台上生成另一个平台上可执行的代码
不同于编译:一个平台生成该平台上可执行代码
交叉编译例子:如c51 keil 在windows上编写代码,在单片机上运行
在linux上面编写代码,并编译生成可执行代码,树莓派上运行
1.平台上不允许或不能安装我们所需要的编译器,比如c51,其目标上的资源匮乏,无法运行我们所需要的环境
2.目标平台还没有建立,连操作系统都没有,操作系统也是代码,也要编译,此时只能交叉编译
宿主机(host):编辑和编译的平台,一般基于x86的PC
目标机(target):用户开发的系统,host编译后生成可执行代码在target上运行
工具:交叉编译器
1.不同的平台所用的交叉编译链不同
对于树莓派,github下载交叉编译工具解压
tools-master/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin arm-linux-gnueabinf-gcc
#此可执行文件为交叉编译工具,相当于gcc
为了便于使用指令,应该设置好环境变量
export PATH=[系统本身具有的环境变量]:[交叉编译工具路径]
echo $APTH #获取当前环境变量
#将上述语句加入/home/hao/.bashrc 末尾即可永久有效,无需每次设置
#设置完毕后,下述命令使配置文件立即生效
source /hone/hao/.bashrc
2.编译后载入目标机运行
scp filename [email protected]:/home/pi/Hao
#互相交互数据,ubuntu上要安装服务端ssh
sudo apt install openssh-server
软链接
1.类似于windows的快捷方式
2.在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息
3.不占用磁盘空间
硬链接
在选定的位置下创建一个大小相同的文件
无论软链接还是硬链接,源文件的变化都会使链接文件发生变化
软链接:
ln -s filename targetname #创建快捷方式
硬链接:
ln filename targetname
1.正常我们先要交叉编译wiringPi库,编译出的库适合树莓派,这时候交叉编译可执行程序,链接的库格式才正确
//链接的库必须也是目标机平台
2.然后再用交叉编译工具来编译,并用-L指定库的路径
arm-linux-gnueabihf-gcc test.c -I /home/hao/WiringPi/WiringPi -lwiringPi -L /home/hao/WiringPi
#用arm-linux-gnueabihf-gcc编译test.c,连接到/home/hao/WiringPi下的-lwiringPi的动态库,头文件在/home/hao/WiringPi/WiringPi下
直接获取适合于树莓派的wiringPi库
#检索当前文件加下所有文件内容含name的文件
grep name * -nir
#检索当前文件夹下*ns文件
find . -name *ns
#列出终端包含name的历史命令
history | grep name
#删除name的文件夹
rm -rf name
#/usr/lib
ls -l | grep wiringPi
#将libwiringPi.so.2.50直接从树莓派复制到x86平台(可以创建一个该文件的软链接),然后就可以直接使用交叉编译工具链接到该库来编译了
uname -r
#查看内核版本
内核源码下载地址:https://github.com/raspberrypi/linux/tree/
C51,STM32(裸机) -> C直接操控底层寄存器实现相关业务 业务流程型的裸机代码
遥控灯: while(1)
垃圾桶:WemosD1 LOOP
恩智浦智能车: stm32
x86:电源->BIOS->windows内核->磁盘->应用程序
嵌入式产品(树莓派等):电源->Bootloader(引导操作系统启动)->Linux内核->文件系统(根据功能性来组织文件夹)->应用程序
Android:电源->fastBoot->linux内核->文件系统->虚拟机(JAVA)->HOME程序
linux是一个开源的支持多架构多平台代码,可执行非常高,包含1.3w个c文件,内核编译出来仅4M
因为支持多平台,多架构,编译前配置成适合的目标平台使用,不是所有代码都参与编译
BootLoader #一阶段 让CPU 跟内存,FLASH, 串口,IIC,IIS, 数据段,打交道,驱动这些设备(汇编和C结合)
#二阶段: 引导Linux内核启动 (纯C)
大约1.3w个C文件 1100w行代码
Linux是开源,免费支持多架构多平台代码,由Linux开源社区工作者爱好者共同维护,可以移植性非常高,但是Linux内核编译出来一般就几M,因为不是所有的代码都参与编译
因为支持多平台,多架构,所以编译之前要配置,配置成适合的目标平台来用
ARM
海思 友善之臂 RK 树莓派 nanoPi
X86
PowerPC
MIPS
**arch:**包含和硬件体系结构相关的代码,每种平台占一个相应的目录。和32位PC相关的代码存放在i386目录下,其中比较重要的包括kernel(内核核心部分)、mm(内存管理)、math-emu(浮点单元仿真)、lib(硬件相关工具函数)、boot(引导程序)、pci(PCI总线)和power(CPU相关状态)
**block:**部分块设备驱动程序
**crypto:**常用加密和散列算法(如AES、SHA等),还有一些压缩和CRC校验算法
**Documentation:**关于内核各部分的通用解释和注释
**drivers:**设备驱动程序,每个不同的驱动占用一个子目录
**fs:**各种支持的文件系统,如ext、fat、ntfs等
**include:**头文件。其中,和系统相关的头文件被放置在linux子目录下
**init:**内核初始化代码(注意不是系统引导代码)
**ipc:**进程间通信的代码
**kernel:**内核的最核心部分,包括进程调度、定时器等,和平台相关的一部分代码放在arch/*/kernel目录下
**lib:**库文件代码
**mm:**内存管理代码,和平台相关的一部分代码放在arch/*/mm目录下
**net:**网络相关代码,实现了各种常见的网络协议
**scripts:**用于配置内核文件的脚本文件
**security:**主要是一个SELinux的模块
**sound:**常用音频设备的驱动程序等
**usr:**实现了一个cpio
驱动代码的编写后需要编译,它的编译需要驱动代码的编译需要一个提前编译好的内核,编译内核就必须配置
配置的最终目标会生成 .config文件,该文件指导Makefile去把有用东西组织成内核
厂家配linux内核源码,比如说买了树莓派,树莓派linux内核源码
第一种方式:
cp 厂家.config .config
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make bcm2709_defconfig
# 指定ARM架构 指定编译器树莓派 主要核心指令
第二种方式:
make menuconfig #一项项配置,通常是基于厂家的config来配置
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make menuconfig
# 指定ARM架构 指定编译器树莓派 主要核心指令
#驱动两种加载方式
#编译进内核 zImage包含了驱动
#M 模块方式生成驱动文件xxx.ko 系统启动后,通过命令inmosd xxx.ko 加载
第三种方式:
#完全自己来
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 zImage modules dtbs
#指定用多少电脑资源进行编译
#zImage生成内核镜像
#modules要生成驱动模块
#dtbs生成配置文件
编译成功后,看到源码树目录多了vmlinux,失败则无此文件,成功后,目标zImage镜像arch/arm/boot底下
#打包zImage成树莓派可用的xxx.img
./scripts/mkknlimg arch/arm/boot/zImage ./kernel_new.img
插入sd卡,它有2个分区,一个是fat分区,存放操作系统内核,另一个是根目录的分区
dmesg
#查看硬件数据
#sdb sdb1 sdb2
挂载sd卡
mkdir data1 data2
#挂载U盘
sudo mount /dev/sdb1 data1 #一个fat分区,是boot相关的内容,kernel的img
sudo mount /dev/sdb2 data2 #一个是ext4分区,也就是系统的根目录分区。
1.安装modules, 设备驱动文件: hdmi usb wifi io …
sudo ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make INSTALL_MOD_PATH=/home/hao/data2 modules_install
2.安装更新 kernel.img 文件,注意镜像名字是kernel7.img
先备份
cd /home/hao/data1
cp kernel7.img kernel7OLD.img
#查看文件拷贝是否损坏
md5sum filename
再把编译新生成的拷贝到data1,起名kernel7.img
cp kernel_new.img /home/hao/data1/kernel7.img
3.拷贝配置文件
cp arch/arm/boot/dts/.*dtb* /home/hao/data1
cp arch/arm/boot/dts/overlays/.*dtb* /home/hao/data1/overlays/
cp arch/arm/boot/dts/overlays/README /home/hao/data1/overlays/
修改cmdline.txt,使串口可以打印终端数据,然后插回树莓派启动系统
1.什么是文件系统?
常规认知: 根目录
文件系统是操作系统用于明确存储设备组织文件的方法,就是文件管理系统(程序),简称文件系统
2.文件系统(文件管理系统的方法)的种类有哪些?
FAT VFAT NTFS EXT1/2/3/4 HFS …
#linux查看文件系统的命令
df -T
#树莓派的文件系统
# vfat: boot(bootloader, kernel)
# ext4: 根目录
# tmpfs: 内存文件系统
3.什么是分区?
windows: 随意(面向普通用户PC),目录即分区,C(装系统的位置)也可以随意在C盘存放文件. D盘(用户随意发挥)
Linux: 按照功能来分区,每个分区严格存放文件(开发者) ,根目录下的文件可能不在一个分区内,它们不同于windows,不是连续的
嵌入式系统可以分为4个区,分别是
bootloader 启动代码
para 启动代码向内核传递参数的位置
kernel 内核分区
根分区等 文件系统结构
4.什么是文件系统目录结构?
常规认知: 根目录,不是分区,和windows不同
在linux系统中,目录被组织成一个单根倒置的树结构,文件系统目录从根目录开始,用/来表示,以 . 开头的为隐藏文件,路径用/来分割(windows下以 \ 来分割)
/*
//根据标准,根目录下的文件夹按功能来划分
1、/boot 该目录默认下存放的是Linux的启动文件和内核。
2、/initrd 它的英文含义是boot loader initialized RAM disk,就是由boot loader初始化的内存盘。在linux内核启动前,boot loader会将存储介质(一般是硬盘)中的initrd文件加载到内存,内核启动时会在访问真正的根文件系统前先访问该内存中的initrd文件系统。
3、/bin 该目录中存放Linux的常用命令。
4、/sbin 该目录用来存放系统管理员使用的管理程序。
5、/var 该目录存放那些经常被修改的文件,包括各种日志、数据文件。
6、/etc 该目录存放系统管理时要用到的各种配置文件和子目录,例如网络配置文件、文件系统、X系统配置文件、设备配置信息、设置用户信息、开机启动等。
7、/dev 该目录包含了Linux系统中使用的所有外部设备,它实际上是访问这些外部设备的端口,访问这些外部设备与访问一个文件或一个目录没有区别。
8、/mnt 临时将别的文件系统挂在该目录下。
9、/root 如果你是以超级用户的身份登录的,这个就是超级用户的主目录。
切换为root用户:
sudo passwd root
su - root
10、/home 如果建立一个名为“xx”的用户,那么在/home目录下就有一个对应的“/home/xx”路径,用来存放该用户的主目录。
11、/usr 用户的应用程序和文件几乎都存放在该目录下。
12、/lib 该目录用来存放系统动态链接共享库,几乎所有的应用程序都会用到该目录下的共享库。
13、/opt 第三方软件在安装时默认会找这个目录,所以你没有安装此类软件时它是空的,但如果你一旦把它删除了,以后在安装此类软件时就有可能碰到麻烦。
14、/tmp 用来存放不同程序执行时产生的临时文件,该目录会被系统自动清理干净。
15、/proc 可以在该目录下获取系统信息,这些信息是在内存中由系统自己产生的,该目录的内容不在硬盘上而在内存里。
16、/misc 可以让多用户堆积和临时转移自己的文件。
17、/lost+found 该目录在大多数情况下都是空的。但当突然停电、或者非正常关机后,有些文件就临时存放在这里。
18、文件颜色的含义:蓝色为文件夹;绿色是可执行文件;浅蓝色是链接文件;红框文件是加了SUID位,任意限权;红色为压缩文件;褐色为设备文件。
*/
虚拟文件系统(Virtual File System)就是对各种文件系统的一个抽象,它为各种文件系统提供了一个通用的接口,
虚拟文件系统有什么作用: 简化应用程序员的开发
不管是什么文件类型,不管文件是磁盘还是设备,都只用open read write统一操作
| 用户态
| 应用程序 c库(提供了应用程序支配内核操作的接口)
| shell:命令解释器,解释用户输入的命令并发送给内核,然后告知用户结果(如打印在终端上)
| 内核态
| 系统调用接口 VFS
| 文件系统 进程控制
| 设备驱动程序
一切皆文件,包括鼠标,屏幕,内存网卡等,各设备以文件形式存放在/dev目录下,称为设备文件,这些文件可以被打开关闭等操作,像操作普通文件一样
open()函数经过虚拟文件系统,以调用驱动程序来实现,每一个硬件都有驱动程序
系统寻找驱动程序:文件名,设备号(包含主设备号和次设备号)
主设备号:用来区分不同类型的设备 次设备号:区分同一类型的多个设备
驱动链表:管理所有的设备驱动,以链表的形式存放在内核中
添加:编写完驱动程序,加载到内核,插入链表的顺序由设备号来检索(设备名,设备号,设备驱动函数[操作寄存器来驱动IO口])
我们也做这两件事,添加驱动和调用驱动
/*
open("/dev/pin",O_RDWR) -> sys_call -> VFS(sys_open) -> 驱动程序
*/
//open调用后会发生软中断,终端号0x80,表示发生了一种系统调用
字符设备:指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序
字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等
/*
字符设备驱动框架
*/
#include //struct file_operations 在此头文件中被定义
#include
#include
#include //struct class、struct device 在此头文件中被定义
#include
#include //dev_t 在此头文件中被定义
#include
static struct class* pin4_class;
static struct device* pin4_class_dev;
static dev_t devno; //设备号
static int major = 231; //主设备号
static int minor = 0; //次设备号
static char* module_name = "pin4"; //模块名
static int pin4_open(struct inode* inode, struct file* file) {
/*上层open后,此函数会被调用*/
printk("pin4_open\n"); //内核打印函数用printk,和printf类似
return 0;
}
static ssize_t pin4_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos) {
/*上层open后,此函数会被调用*/
int cmd;
copy_from_user((void*)&cmd, buf, count); //此函数将上层write的数据写入cmd,cmd为指向void类型的指针
//cope_from_user同理,用于pin4_read
printk("pin4_write\n");
return 0;
}
static struct file_operations pin4_fops = {
/*该结构体会被注册到驱动列表,主要包含了上层能实现的函数调用列表*/
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
};
int __init pin4_drv_init(void) {
//真实驱动入口
int ret;
devno = MKDEV(major, minor); //创建设备号,根据主次设备号自动生成
ret = register_chrdev(major, module_name, &pin4_fops); //注册驱动,告诉内核,把这个驱动加入到内核的链表中
pin4_class = class_create(THIS_MODULE, "myfirstdemo"); // 让代码在dev下自动生成设备
pin4_class_dev = device_create(pin4_class, NULL, devno, NULL, module_name); //创建设备文件
return 0;
}
void __exit pin4_drv_exit(void) {
device_destroy(pin4_class, devno);
class_destroy(pin4_class);
unregister_chrdev(major, module_name); //卸载驱动,即从驱动链表中删除
}
module_init(pin4_drv_init); //入口,内核加载驱动的时候,这个宏会被调用(调用pin4_drv_init)
module_exit(pin4_drv_exit); //卸载驱动会被调用
MODULE_LICENSE("GPL v2");
打开linux源码树目录,进入drivers/char,将驱动代码复制到此目录
为了编译时将其编译,需要修改Makefile文件
#添加
obj-m += drivername.o
开始编译
#进入源码树目录
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
加载驱动
编译完成后将drivername.ko上传到树莓派,进行加载
sudo insmod drivername.ko
#加载后在dev下将有pin4文件夹,执行调用module_init(pin4_drv_init)进行将驱动加入到驱动链表
#此时该文件不允许被程序访问,将其设置为所有用户任何均可访问和读写
sudo chmod 666 /dev/pin4
测试代码
运行含有open该驱动的代码
dmesg | grep pin4 #将可看到在内核态中打印的内容
lsmod #列出驱动模块
sudo rmmod modname #卸载某个驱动
微机总线地址:地址总线 (Address Bus,又称:位址总线) 属于一种电脑总线 (一部份),是由CPU 或有DMA 能力的单元,用来沟通这些单元想要存取(读取/写入)电脑内存元件/地方的实体位址
简而言之:cpu 能够访问的内存范围
物理地址:硬件的实际地址或绝对地址
虚拟地址:又称逻辑地址,基于算法的(软件层面的)地址,编程时使用的是虚拟地址,用宏将物理地址转化为虚拟地址,将寄存器映射成普通内存单元来访问
#include
#define ioremap(cookie,size) __ioremap(cookie,size,0)
/*
函数将物理地址指向的寄存器映射成普通内存单元,并返回虚拟地址
phys_addr:要映射的起始的IO地址
size:要映射的空间的大小
flags:要映射的IO空间和权限有关的标志
*/
void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);
iounmap同理,参数是虚拟地址,用于解除寄存器对该虚拟地址的映射
cat /proc/xxxinfo 查看硬件信息
#如查看cpu信息
cat /proc/cpuinfo
General Purpose I/O (GPIO)
There are 54 general-purpose I/O (GPIO) lines split into two banks. All GPIO pins have at least two alternative functions within BCM
The GPIO has 41 registers. All accesses are assumed to be 32-bit
GPIO Function Select Registers (GPFSELn)
The function select registers are used to define the operation of the general-purpose I/O pins, The FSEL{n} field determines the
functionality of the nth GPIO pin. All unused alternative function lines are tied to ground and will output a “0” if selected. All pins reset
to normal GPIO input operation
GPIO Pin Output Set Registers (GPSETn)
The SET{n} field defines the respective GPIO pin to set, writing a “0” to the field has no effect. If the GPIO pin is being used as in input
(by default) then the value in the SET{n} field is ignored. However, if the pin is subsequently defined as an output then the bit will be set
according to the last set/clear operation
GPIO Pin Output Clear Registers (GPCLRn)
The output clear registers) are used to clear a GPIO pin. The CLR{n} field defines the respective GPIO pin to clear, writing a “0” to the
field has no effect. If the GPIO pin is being used as in input (by default) then the value in the CLR{n} field is ignored. However, if the pin
is subsequently defined as an output then the bit will be set according to the last set/clear operation
补码:当将一个十进制正整数转换为二进制数的时候,只需要通过除2取余的方法即可,但是将一个十进制的负整数转换为二进制是以补
码的形式表示,其转换方式是:先按正数转换,然后取反加1
按位与(&):参加运算的两个数,换算为二进制后,进行与运算,当相应位上的数都是1时,该位才取1,否则该为为0
按位或(|):参加运算的两个数,换算为二进制后,进行或运算,只要相应位上存在1,那么该位就取1,均不为1,即为0
按位异或(^):参加运算的两个数,换算为二进制后,进行异或运算,只有当相应位上的数字不相同时,该为才取1,若相同,即为0
取反(~):参加运算的两个数,换算为二进制后,进行取反运算,每个位上都取相反值,1变成0,0变成1
左移(<<):参加运算的两个数,换算为二进制后,进行左移运算,用来将一个数各二进制位全部向左移动若干位,新位补0
左移一位的结果就是原值乘2,左移两位的结果就是原值乘4
右移(>>):参加运算的两个数,换算为二进制后,进行右移运算,用来将一个数各二进制位全部向右移动若干位,新位补0
右移一位的结果就是原值除2,左移两位的结果就是原值除4(除了以后没有小数位的,都是取整)
/*
树莓派pin4设置为输出引脚的驱动
*/
/*
字符设备驱动框架
*/
#include //struct file_operations 在此头文件中被定义
#include
#include
#include //struct class、struct device 在此头文件中被定义
#include
#include //dev_t 在此头文件中被定义
#include
static struct class* pin4_class;
static struct device* pin4_class_dev;
static dev_t devno; //设备号
static int major = 231; //主设备号
static int minor = 0; //次设备号
static char* module_name = "pin4"; //模块名
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0 = NULL;
volatile unsigned int* GPCLR0 = NULL;
static int pin4_open(struct inode* inode, struct file* file) {
/*上层open后,此函数会被调用*/
printk("pin4 OUTPUT");
*GPFSEL0 &= ~(0x6 << 12);
*GPFSEL0 |= (0x1 << 12);
printk("pin4_open\n"); //内核打印函数用printk,和printf类似
return 0;
}
static ssize_t pin4_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos) {
/*上层open后,此函数会被调用*/
int cmd;
copy_from_user((void*)&cmd, (void*)buf, count);
printk("%d",cmd);
if(cmd == 1) {
printk("pin4 HIGH");
*GPSET0 |= 0x1 << 4;
}else if(cmd == 0){
printk("pin4 LOW");
*GPCLR0 |= 0x1 << 4;
}else{
printk("undo");
}
printk("pin4_write\n");
return 0;
}
static struct file_operations pin4_fops = {
/*该结构体会被注册到驱动列表,主要包含了上层能实现的函数调用列表*/
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
};
int __init pin4_drv_init(void) {
//真实驱动入口
int ret;
devno = MKDEV(major, minor); //创建设备号,根据主次设备号自动生成
ret = register_chrdev(major, module_name, &pin4_fops); //注册驱动,告诉内核,把这个驱动加入到内核的链表中
pin4_class = class_create(THIS_MODULE, "myfirstdemo"); // 让代码在dev下自动生成设备
pin4_class_dev = device_create(pin4_class, NULL, devno, NULL, module_name); //创建设备文件
GPFSEL0 = (volatile unsigned int*)ioremap(0x3f200000,4);
GPSET0 = (volatile unsigned int*)ioremap(0x3f20001C,4);
GPCLR0 = (volatile unsigned int*)ioremap(0x3f200028,4);
return 0;
}
void __exit pin4_drv_exit(void) {
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);
device_destroy(pin4_class, devno);
class_destroy(pin4_class);
unregister_chrdev(major, module_name); //卸载驱动,即从驱动链表中删除
}
module_init(pin4_drv_init); //入口,内核加载驱动的时候,这个宏会被调用(调用pin4_drv_init)
module_exit(pin4_drv_exit); //卸载驱动会被调用
MODULE_LICENSE("GPL v2");
运行上层应用,可通过 gpio readall 来查看 mode 和 v