前言
本文主要讲单片机外设的功能,即这些外设是什么,可以用来干什么,了解了之后我们就可以通过相应的寄存器配置来驱动这些外设。本文带大家深入了解一下这些外设的工作原理,知道了功能之后,对应任意一个MCU都可以找相应功能的寄存器。因为寄存器名字可以不同,配置方式可以不同,但是功能不可能有很大的差异。这样才能在换一个平台MCU的情况下,实现快速入手
一、时钟与中断
在之前首先了解一下寄存器,与远古时代的汇编不同,现在都是C语言操作寄存器。那寄存器是什么,百度百科说的很笼统,我们可以将寄存器比做成可以被软件控制的开关,通过不同的开关组合状态,就可以形成我们想要的功能。
然后我们再了解一下时钟,学过数电的都知道,系统内部改变状态都需要一个CLK脉冲信号,这边到MCU就是一个时钟信号。以下所有外设都需要时钟的支持,当然我们可以通过寄存器和选择器来配置系统时钟,并对其作相应的分频和倍频,来得到我们需要的时钟频率。以下就是M4内核的时钟树结构:
不过时钟不需要太关心,我们只需要知道常用的主频是多少Hz就行,因为对于像STM32之类,使用库函数都被官方封装好了时钟,然后被启动文件直接上电执行。如果是RISC或者STM8内核之类,其实配置时钟就一两个寄存器,一般就是主频选择和时钟分频。其他如51内核的,一般情况下不需要配置时钟,MCU默认开启,除非需要修改时钟的情况下才需要配置。
有了上面的基础就可以开始中断部分了,首先要知道中断是什么?中断是一种发生了一个外部的事件时调用相应的处理程序的过程。这个概念可能不好理解,那我们直接需要知道就是中断可以用来干什么,怎么来触发中断的就行。正常情况下,计算机所有指令都是随着系统时钟从上到下执行,但是中断可以在系统指令执行期间优先执行。
通常情况下,中断需要一个触发源,即触发中断的信号,如外部中断、定时器、ADC等等,出发后程序运行进入中断函数,原主函数位置会保存到栈空间,等中断函数执行完成之后,再从栈空间读取继续主函数的运行。
上图就是STM32的中断控制器,其他MCU也很类似,由上大概可以知道需要配置的寄存器如下,然后再对比参考手册找相应的寄存器:
- 中断触发源:即触发中断信号的来源
- 中断优先级:部分MCU需要配置中断优先级,即高优先级的中断优先处理
- 中断触发类型:比如在上升沿触发
- 中断使能:即开启中断功能
- 中断标志位:主要是触发中断和中断完成标志位,需要软件判断和设置
二、GPIO
GPIO( general purpose intput output) 是通用输入输出端口的简称。那什么是输入输出呢?我们都知道芯片只能处理数字信号,数字信号在数据上体现为逻辑0和逻辑1,实际上代表了高电平(5V或3.3V)和低电平(0V)两种状态。那输入就是MCU通过I/O口来检测外部电平状态,而输出就是MCU向外部输出不同的电平状态。
知道I/O的功能之后,我们就可以来配置GPIO,需要配置的参数如下:
- I/O时钟
ARM系列需要单独开启IO时钟,其他如51、STM8、RISC等不需要配置,只需要初始化系统时钟即可。 - 方向控制
方向控制就是配置IO口输入输出模式,基本所有MCU都需要配置 - 输入模式配置
当IO被用于输入模式时,一般可设置为上拉、下拉或浮空模式。所谓的上拉就是接一个上拉电阻,上拉电阻连接正极,提供5V或者3.3V电压,即默认高电平。浮空模式就是没有高低电平状态,I/O口电压跟随周围电路状态 - 输出模式配置
当IO被用于输出模式时,一般可设置为推挽、开漏或准双向口。准双向口和推挽的区别是,推挽是强上下拉,可以提供更大的电流,但是准双向口可以进行输入输出。开漏输出时会关闭内部上拉电阻,只能下拉输出,上拉输出需要外部加上拉电阻。由于开漏相当于MOS开关,所以可以用于IIC电路电平匹配 - 数据寄存器
当I/O口作为输入的时候,可以读取数据寄存器,查看I/O口高低电平状态;当I/O口作为输出时,将数据写进数据寄存器,此时对应的引脚就可以输出相应的电平状态 - I/O复用与映射
I/O复用是一个很常见的功能,就是将普通的I/O口当作其他外设使用,比如复用为ADC可以当作模拟输入,复用给定时器作为PWM输出等等;而映射就是当复用的I/O引脚不够用的时候,可以把其他外设比如ADC的某个通道,映射到特定的引脚,原来的I/O作为其他用途
三、ADC
首先要知道ADC是什么?ADC,Analog-to-Digital Converter的缩写,指模/数转换器或者模数转换器,是指将连续变化的模拟信号转换为离散的数字信号的器件。
那ADC有什么用呢?我们在电路中通常会使用一些传感器,这些传感器可以将一些模拟量转换成不同的阻值,比如温度传感器,他的阻值会随着温度的变换而变化,此时我们只要测量出阻值就可以根据线性表得出温度。而ADC最基本的作用就是测量电压,然后我们就可以根据电压值计算出阻值,从而得到最终温度。
上图就是STM32内部ADC框架,还是很复杂的,不过一些MCU的ADC没有那么多功能,我们可以去掉上图的3和7,以一个RISC的内部ADC框架来说明,如下图:
这样了解ADC就会简单点,ADC具体配置如下:
- ADC时钟配置
- 使能ADC时钟,即开启ADC
- 选择时钟源,即时钟信号的来源,比如系统时钟,内部晶振,外部晶振
- 进行时钟分频
- 采样延迟,防止采样抖动,可以选择延迟一段时间后采样
- 通道选择
- I/O配置:使用ADC采样一定是用的某个I/O口,将其设置成模拟输入模式
- 通道选择:每个ADC几个通道,分别对应不同的I/O口,也可以使用I/O映射功能
- 触发模式选择
- 触发源:一般可以定时器中断触发,ADC中断触发,或者软件触发
- 触发方式:一般有上升沿、下降沿、低电平等等之类触发
-
参考电压选择
参考电压是什么?参考电压就是ADC测量的范围,一般用Vref-和Vref+两个参考元组成,而输入电压Vin的范围就是:Vref- ≤ Vin ≤ Vref+ 。
那我们通常把参考电压的负极接地,即Vref- = 0V;然后将参考电压正极接内部或者外部的其他电源,如2V、3.3V或者5V电压 -
数据对齐与处理
在了解数据处理之前,先了解一下ADC最重要的参数,就是ADC的精度。通常MCU的ADC精度是12位的,当然有些专用ADC可能有更高的精度。那为什么要了解精度呢?我们要知道通常MCU内部数据为8位或者8的倍数位,那此时12位数据用8位明显不够,用16位又多了。此时就需要配置数据对其方式,16位左对齐那么低4位数据无效;同理右对齐那么高4位的数据无效。
我们配置好之后,直接去读取的数据只是ADC的数据,而不是实际电压值。那这电压值怎么换算呢?这里就需要综合ADC精度和参考电压了,实际电压值=采样值/(2^精度)*(Vref+ - Vref-)+ Vref-,这个公式应该不难理解,就是根据参考电压,然后按照比例得出电压值。而我们精度12位就是4096,Vref-接地为0V,所以最终电压计算如下所示:
四、定时器
定时器最基本的功能就是定时与计数了,这有什么用呢?我们都知道可以软件里写一个死循环用来延迟,但是这种延迟会占系统CPU资源,而且会被中断打断,导致延迟不准确。此时我们可以用定时器设置计数标志位,从而可以得到精准的延迟。首先我们看一下定时器分类,定时器基本有以下3种类型:
- 基本定时器:基本定时器是最简单的定时器,只有计数和定时作用,可以设置中断
- 通用定时器:在基本定时器的基础上,再加上捕获/比较功能,一般用来PWM输出
- 高级定时器:它一般有4路捕获/比较通道,此外还有编码器接口与刹车中断,一般用来电机控制
正常情况下,如ARM、STM8、RISC之类PWM是由通用定时器产生的。而8051定时器没有PWM功能,其他如STC8H用的51内核,它的PWM是额外的专用定时器。本小节就讲常规的基本定时器与通用定时器功能
4.1 基本定时器
首先看一下基本定时器框图,为了简单,这边选了一个RISC的基本定时器:
从上图很容易看出,先是时钟进来分频,然后计数器就开始进行计数,基本定时器一般只支持递增计数,当计数值CNT与影子寄存器值相等时,计数器就会清零并产生更新事件,然后自动重装寄存器ARR就把值写入影子寄存器。如果使能中断,触发更新事件就会产生中断,然后我们可以在中断里清除中断标志位。工作方式如下所示:
大致的工作原理如上,根据流程我们需要配置的参数如下:
- 时钟配置
- 选择相应的定时器,并使能定时器时钟
- 选择时钟源(有些MCU是默认的,比如STM32)
- 设置分频系数
- 计数设置
对于基本定时器只需要设置自动重载寄存器ARR的值即可。此时定时器产生更新事件的时间,即定时时间=系统时钟/分频系数/(计数值+1)。因为计数器是从0开始计数
4.2 通用定时器
这边同样用一个RISC的通用定时器举例,框图如下所示:
可以看出与基本定时器明显的区别就是多了几路捕获/比较通道。此外通用计数器的位数更高,并且有些通用定时器可以递减计数。那这边有个问题,捕获和比较分别是什么呢?下面会重点介绍输出比较,简略介绍输入捕获功能
4.2.1 输入捕获
输入捕获一般应用在两个方面,一个方面是脉冲跳变沿时间测量,另一方面是 PWM 输入测量。
- 测量脉宽或者频率
当测量频率时,可以记录两次上升沿的时间;如果是测量脉宽就记录三个值,其中v3-v1是周期,v2-v1是脉宽 - PWM 输入模式
测量脉宽和频率还有一个更简便的方法就是使用 PWM 输入模式。与上面那种只使用一个捕获寄存器测量脉宽和频率的方法相比,PWM 输入模式需要占用两个捕获寄存器。以STM32寄存器举例:
下面我们以一个更加具体的时序图来分析下 PWM 输入模式:
PWM 信号由输入通道 TI1 进入,配置 TI1FP1 为触发信号,上升沿捕获。当上升沿的时候 IC1 和IC2 同时捕获,计数器 CNT 清零,到了下降沿的时候,IC2 捕获,此时计数器 CNT 的值被锁存到捕获寄存器 CCR2 中,到了下一个上升沿的时候,IC1 捕获,计数器 CNT 的值被锁存到捕获寄存器 CCR1 中。其中 CCR2 测量的是脉宽,CCR1 测量的是周期。
4.2.2 输出比较
在STM32F4输出比较模式总共有 8 种,具体的由寄存器 CCMRx 的位 OCxM[2:0] 配置。我们这里只讲解最常用的 PWM 模式。
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。我们可以让定时器产生PWM,在计数器频率固定时,PWM 频率或者周期由自动重载寄存器(TIMx_ARR)的值决定,其占空比由捕获/比较寄存器(TIMx_CCRx)的值决定。PWM 产生原理示意图如下图所示:
PWM 模式分为两种,PWM1 和 PWM2,总得来说是差不多:
定时器产生PWM的方式有许多种,下面我们以边沿对齐模式(即递增计数模式/递减计数模式)为例,PWM模式1或者 PWM 模式2产生PWM的示意图,如下图所示:
有了上面的基础就可以开始配置PWM了,步骤如下:
- I/O配置
由于PWM输出使用的是定时器的某个通道,这个通道对应某个I/O口,所以要先开启对应I/O的复用或者映射,并将其配置为输出模式 - 定时器配置
定时器配置可以参考4.1小节的基本定时器的配置 - 使能PWM输出比较通道,并选择PWM模式
这个PWM输出通道指的是定时器捕获/比较通道,比如上面通用定时器框图里的CH1。至于PWM模式上面有讲 - 使能比较输出并配置输出极性
这里的比较输出使能是指主通道或者互补通道的使能,输出极性就是在对应的PWM模式下,当有效时输出电平为高电平还是低电平 - 配置比较值
这里的比较值就是PWM脉宽,即上面捕获比较值寄存器CCR。通常CCR需要小于自动重载值ARR,ARR配置就相当于PWM的周期 - 按需求使能输出比较中断
五、UART
介绍串口UART之前先介绍一下通讯的基本概念
5.1 通讯的基本概念
5.1.1 串行通讯与并行通讯
按数据通信方式分类,可分为串行通信和并行通信两种。串行和并行的对比如下图所示:
串行通信的基本特征是数据逐位顺序依次传输,优点是传输线少、布线成本低、灵活度高等优点,一般用于近距离人机交互,特殊处理后也可以用于远距离,缺点就是传输速率低。
而并行通信是数据各位可以通过多条线同时传输,优点是传输速率高,缺点就是布线成本高,抗干扰能力差,适用于短距离、高速率的通信。
5.1.2 全双工、半双工及单工通讯
根据数据传输方向,通信又可分为全双工、半双工和单工通信。全双工、半双工和单工通信的比较如下图所示:
5.1.3 同步通讯与异步通讯
根据数据同步方式,通信又可分为同步通信和异步通信。同步通信和异步通信比较如下图所示:
同步通信要求通信双方共用同一时钟信号,在总线上保持统一的时序和周期完成信息传输。优点:可以实现高速率、大容量的数据传输,以及点对多点传输。缺点:要求发送时钟和接收时钟保持严格同步,收发双方时钟允许的误差较小,同时硬件复杂。
异步通信不需要时钟信号,而是在数据信号中加入开始位和停止位等一些同步信号,以便使接收端能够正确地将每一个字符接收下来,某些通信中还需要双方约定传输速率。优点:没有时钟信号硬件简单,双方时钟可允许一定误差。缺点:通信速率较低,只适用点对点传输。
5.1.4 通信速率
在数字通信系统中,通信速率(传输速率)指数据在信道中传输的速度,它分为两种:传信率和传码率。
- 传信率:每秒钟传输的信息量,即每秒钟传输的二进制位数,单位为 bit/s(即比特每秒),因而又称为比特率。
- 传码率:每秒钟传输的码元个数,单位为 Baud(即波特每秒),因而又称为波特率。
比特率和波特率的关系可以用以下式子表示:比特率 = 波特率 * log2M
其中 M 表示码元承载的信息量。我们也可以理解 M 为码元的进制数。
5.2 异步串口UART
有些MCU是USART意为同步异步串口,而UART是异步全双工串口。串口通讯 (Serial Communication) 是一种设备间非常常用的串行通讯方式,串口按位(bit)发送和接收字节。串口通信协议是指规定了数据包的内容,内容包含了起始位、主体数据、校验位及停止位,双方需要约定一致的数据包格式才能正常收发数据的有关规范。在串口通信中,常用的协议包括RS-232、RS-422 和 RS-485 等。
5.2.1 物理层
根据通讯使用的电平标准不同,串口通讯可分为 TTL 标准及 RS-232 标准。通常情况下,MCU引脚发出的UART信号为TTL信号,使用 RS-232 标准的串口设备间常见的通讯结构:
在上面的通讯方式中,两个通讯设备的“DB9 接口”之间通过串口信号线建立起连接,串口信号线中使用“RS-232 标准”传输数据信号。由于 RS-232 电平标准的信号不能直接被控制器直接识别,所以这些信号会经过一个“电平转换芯片”转换成控制器能识别的“TTL 校准”的电平信号,才能实现通讯。
两者电平信号区别如下图:
RS-232 与 TTL 电平标准下表示同一个信号
5.2.2 协议层
STM32中的串口通讯的数据包由发送设备通过自身的 TXD 接口传输到接收设备的 RXD 接口。在串口通讯的协议层中,规定了数据包的内容,它由启始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据:
异步通信时接收端一般会用采样定理判断信号逻辑
5.3 串口配置
为了结构简单,这边选取了一个RISC的UART框图:
UART通常会用到4根线
- VCC:电压源,一般用来给模块供电,模块自己有电可以省略
- GND:地线,必要的信号,用来同步电平
- RX:串行数据输入。
- TX:串行数据输出。当发送器使能,但不发送数据时,TX 引脚处于高电平。当发送器使能,且发送数据时,TX 引脚在起始位期间处于低电平,在停止位期间处于高电平。
UART的配置步骤如下:
- 引脚配置:主要是将RX、TX配置成对应串口的输入输出引脚,并开启对应的复用或者映射
- 使能相应的UART串口时钟
- 配置波特率,常用的有115200和9600
我们知道异步串口是没有同步信号CLK的,那接收端只能靠波特率来同步信号。波特率,即每秒钟传输的码元个数,在二进制系统中(串口的数据帧就是二进制的形式),波特率与波特率的数值相等,所以我们今后在把串口波特率理解为每秒钟传输的二进制位数。
波特率的配置一般有两种方式根据不同的MCU。一种是配置寄存器,根据MCU波特率计算公式,对UART时钟进行分频得到对应波特率。还有一种是类似STM32直接调用库函数,通过输入波特率,然后库函数自己计算对应的波特率配置寄存器值 - 设置通信数据长度,常规的为8位
- 设置奇偶校验位,常规为无奇偶校验
- 设置停止位长度,一般为1位
- 使能发送和接受功能
- 根据需求使能中断,如果不使能中断,系统就会轮询串口接收缓冲区
六、IIC
6.1 IIC简介
I2C通讯协议 (Inter-Integrated Circuit) 是由 Phiilps 公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、CAN 等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路 (IC) 间的通讯。
6.1.1 IIC物理层
IIC总线是两线式串行总线,用于连接微控制器以及其外围设备。它是由数据线 SDA 和时钟线 SCL 构成的串行总线,可发送和接收数据,在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送,很明显IIC是同步全双工串行口。IIC 总线有如下特点:
- 总线由数据线 SDA 和时钟线 SCL 构成的串行总线,数据线用来传输数据,时钟线用来同步数据收发。
- 总线上每一个器件都有一个唯一的地址识别,所以我们只需要知道器件的地址,根据时序就可以实现微控制器与器件之间的通信。
- 数据线 SDA 和时钟线 SCL 都是双向线路,都通过一个电流源或上拉电阻连接到正的电压,所以当总线空闲的时候,这两条线路都是高电平。
- 总线上数据的传输速率在标准模式下可达 100kbit/s 在快速模式下可达 400kbit/s 在高速模式下可达 3.4Mbit/s。
- 总线支持设备连接。可以有多个具备 IIC 通信能力的设备挂载在上面,连接到总线的接口数量只由总线电容 400pF 的限制决定。
- 在使用 IIC 通信总线时,同时支持多个主机和多个从机。多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
IIC总线挂载多个器件的示意图,如下图所示:
6.1.2 IIC协议层
下面来学习 IIC 总线协议,IIC 总线时序图如下所示:
简化一下为1帧数据:
下面就以上图的6个部分讲解:
- 起始信号①与停止信号②
当 SCL 为高电平期间,SDA 由高到低的跳变。起始信号是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在起始信号产生后,总线就处于被占用状态,准备数据传输。
当 SCL 为高电平期间,SDA 由低到高的跳变。停止信号也是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在停止信号发出后,总线就处于空闲状态。
- 数据有效性④
SDA 数据线在 SCL 的每个时钟周期传输一位数据。传输时,SCL 为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据“1”,为低电平时表示数据“0”。当 SCL 为低电平时,SDA的数据无效,一般在这个时候 SDA 进行电平切换。数据在 SCL 的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定。
- 数据传输⑤
在 I2C 总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在 SCL 串行时钟的配合下,在 SDA 上逐位地串行传送每一位数据。数据位的传输是边沿触发。 - 响应③
I2C 的数据和地址传输都带响应。响应包括“应答 (ACK)”和“非应答 (NACK)”两种信号。作为数据接收端时,当设备 (无论主从机) 接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答 (ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答 (NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。
传输时主机产生时钟,在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接收端控制SDA,若 SDA 为高电平,表示非应答信号 (NACK),低电平表示应答信号 (ACK)。 - 空闲状态⑥
IIC 总线的 SDA 和 SCL 两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
6.1.3 IIC读写通讯
IIC 基本的读写通讯过程,包括主机写数据到从机即写操作,主机到从机读取数据即读操作。下面先看一下写操作通讯过程图:
主机首先在 IIC 总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的数据。主机接着发送从机地址+0(写操作)组成的 8bit 数据,所有从机接收到该 8bit 数据后,自行检验是否是自己的设备的地址,假如是自己的设备地址,那么从机就会发出应答信号。主机在总线上接收到有应答信号后,才能继续向从机发送数据。注意:IIC 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
接着讲解一下 IIC 总线的读操作过程:
主机向从机读取数据的操作,一开始的操作与写操作有点相似,观察两个图也可以发现,都是由主机发出起始信号,接着发送从机地址+1(读操作)组成的 8bit 数据,从机接收到数据验证是否是自身的地址。那么在验证是自己的设备地址后,从机就会发出应答信号,并向主机返回 8bit 数据,发送完之后从机就会等待主机的应答信号。假如主机一直返回应答信号,那么从机可以一直发送数据,也就是图中的(n byte + 应答信号)情况,直到主机发出非应答信号,从机才会停止发送数据。
I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址(SLAVE_ADDRESS) 来查找从机。I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向,它是数据方向位 (R/),第 8位或第 11 位。数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据。
读数据方向时,主机会释放对 SDA 信号线的控制,由从机控制 SDA 信号线,主机接收信号,写数据方向时,SDA 由主机控制,从机接收信号。
6.2 软件模拟I2C
使用 IIC 传输数据的配置步骤:
- IO配置
由于软件模拟IC是通过两个IO口的输出来当作SDA和SCL,所以需要配置两个IO口为输出模式,SCL 推挽输出 SDA 开漏输出 - 参考 IIC 总线协议,编写信号函数(起始信号,停止信号,应答信号)
- 编写 IIC 的读写函数
//定义引脚
#define IIC_SCL PB8
#define IIC_SDA PB9
#define READ_SDA PB9 //输入SDA
//IO方向设置
#define SDA_IN() {GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=0<<9*2;} //PB9输入模式
#define SDA_OUT() {GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=1<<9*2;} //PB9输出模式
//定义IIC软件延迟
void delay_us(u8 time)
{
uint16 j,i;
for(j=0;j<time;j++)
{
for(i=0;i<7;i++);
}
}
//产生IIC起始信号
void IIC_Start(void)
{
SDA_OUT(); //sda线输出
IIC_SDA=1;
IIC_SCL=1;
delay_us(4);
IIC_SDA=0;//START:when CLK is high,DATA change form high to low
delay_us(4);
IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
}
//产生IIC停止信号
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
IIC_SCL=0;
IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
delay_us(4);
IIC_SCL=1;
IIC_SDA=1;//发送I2C总线结束信号
delay_us(4);
}
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA设置为输入
IIC_SDA=1;delay_us(1);
IIC_SCL=1;delay_us(1);
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL=0;//时钟输出0
return 0;
}
//产生ACK应答
void IIC_Ack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//不产生ACK应答
void IIC_NAck(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
IIC_SDA=(txd&0x80)>>7;
txd<<=1;
delay_us(2); //对TEA5767这三个延时都是必须的
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(1);
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
以上有几点注意点:
- IIC_SCL和IIC_SDA两个IO口初始化配置为上拉状态,传输结束后也是上拉状态
- 函数SDA_IN() 和SDA_OUT()是配置IO的输入输出,如果IO初始化配置为准双向口,比如在51单片机里可以注释掉
- 函数delay_us()里的延迟,目的就是控制 IIC 的读写速度,通过示波器检测读写速度在 250KHz 内,所以一秒钟传送 500Kb 数据,即一个 bit 位需要 2us,在这个延时时间内可以让器件获得一个稳定性的数据采集。实际上速度控制在100~400KHz通常都能正常通讯
- 在进行IC通讯时,由于是软件模拟信号,最好关闭其他中断,防止对读写产生干扰
七、SPI
7.1 SPI简介
SPI 是英语 Serial Peripheral interface 缩写,顾名思义就是串行外围设备接口。SPI 通信协议是 Motorola 公司首先在其 MC68HCXX 系列处理器上定义的。SPI 接口是一种高速的全双工同步的通信总线,已经广泛应用在众多 MCU、存储芯片、AD 转换器和 LCD 之间。
7.1.1 SPI物理层
SPI 通讯设备之间的常用连接方式见图:
SPI通讯使用 3 条总线及片选线:
- SS ( Slave Select):从设备选择信号线,常称为片选信号线,也称为 NSS、CS,以下用 NSS 表示。
当有多个 SPI 从设备与 SPI 主机相连时,设备的其它信号线 SCK、MOSI 及 MISO 同时并联到相同的 SPI 总线上,即无论有多少个从设备,都共同只使用这 3 条总线;而每个从设备都有独立的这一条 NSS 信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。I2C 协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而 SPI 协议中没有设备地址,它使用 NSS 信号线来寻址,当主机要选择从设备时,把该从设备的 NSS 信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行 SPI 通讯。所以 SPI 通讯以 NSS 线置低电平为开始信号,以 NSS 线被拉高作为结束信号。 - SCK (Serial Clock):时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,如 STM32 的 SPI 时钟频率最大为 fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。
- MOSI (Master Output,Slave Input):主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。
- MISO(Master Input,,Slave Output):主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。
7.1.2 SPI协议层
与 I2C 的类似,SPI 协议定义了通讯的起始和停止信号、数据有效性、时钟同步等环节:
这是一个主机的通讯时序。NSS、SCK、MOSI 信号都由主机控制产生,而 MISO 的信号由从机产生,主机通过该信号线读取从机的数据。MOSI 与 MISO 的信号只在 NSS 为低电平的时候才有效,在 SCK 的每个时钟周期 MOSI 和 MISO 传输一位数据。
- 通讯的起始和停止信号
在图 SPI 通讯时序中的标号处,NSS 信号线由高变低,是 SPI 通讯的起始信号。NSS 是每个从机各自独占的信号线,当从机在自己的 NSS 线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。在图中的标号处,NSS 信号由低变高,是 SPI 通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。 - 数据有效性
SPI 使用 MOSI 及 MISO 信号线来传输数据,使用 SCK 信号线进行数据同步。MOSI 及 MISO 数据线在 SCK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或 LSB 先行并没有作硬性规定,但要保证两个 SPI 通讯设备之间使用同样的协定,一般都会采用图 SPI 通讯时序 中的 MSB 先行模式。
观察图中的标号处,MOSI 及 MISO 的数据在 SCK 的上升沿期间变化输出,在 SCK 的下降沿时被采样。即在 SCK 的下降沿时刻,MOSI 及 MISO 的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI 及 MISO 为下一次表示数据做准备。SPI 每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制。
7.1.3 SPI工作模式
SPI 通信协议就具备 4 种工作模式,在讲这 4 种工作模式前,首先先知道两个单词 CPOL 和 CPHA。
- CPOL,详称 Clock Polarity,就是时钟极性,即主从机没有数据传输的时候(空闲状态)SCL 线的电平状态。假如空闲状态是高电平,CPOL=1;若空闲状态时低电平,那么 CPOL = 0。
- CPHA,详称 Clock Phase,就是时钟相位。时钟相位 CPHA 是指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在SCK 时钟线的“奇数边沿”被采样。当 CPHA=1 时,数据线在 SCK 的“偶数边沿”采样。
由于 CPOL 和 CPHA 都有两种不同状态,所以 SPI 分成了 4 种模式。我们在开发的时候,使用比较多的是模式 0 和模式 3。
SPI 工作模式 | CPOL | CPHA | SCL 空闲状态 | 采样边沿 | 采样时刻 |
---|---|---|---|---|---|
0 | 0 | 0 | 低电平 | 上升沿 | 奇数边沿 |
1 | 0 | 1 | 低电平 | 下降沿 | 偶数边沿 |
2 | 1 | 0 | 高电平 | 下降沿 | 奇数边沿 |
3 | 1 | 1 | 高电平 | 上升沿 | 偶数边沿 |
下面分别对 SPI 的 4 种工作模式进行分析:
- 串行时钟的奇数边沿上升沿采样
我们分析一下 CPOL=0&&CPHA=0 的时序,上图就是串行时钟的奇数边沿上升沿采样的情况,首先由于配置了 CPOL=0,可以看到当数据未发送或者发送完毕,SCL 的状态是低电平,再者 CPHA=0 即是奇数边沿采集。所以传输的数据会在奇数边沿上升沿被采集,MOSI和 MISO 数据的有效信号需要在 SCK 奇数边沿保持稳定且被采样,在非采样时刻,MOSI 和MISO 的有效信号才发生变化。 - 串行时钟的偶数边沿下降沿采样
现在分析一下 CPOL=0&CPHA=1 的时序,图是串行时钟的偶数边沿下降沿采样的情况。由于 CPOL=0,所以 SCL 的空闲状态依然是低电平,CPHA=1 数据就从偶数边沿采样,至于是上升沿还是下降沿,从上图就可以知道,是下降沿。这里有一个误区,空闲状态是低电平的情况下,不是应该上升沿吗,为什么这里是下降沿?首先我们先明确这里是偶数边沿采样,那么看图就很清晰,SCL 低电平空闲状态下,上升沿是在奇数边沿上,下降沿是在偶数边沿上。 - 串行时钟的奇数边沿下降沿采样
图这种情况和第一种情况相似,只是这里是 CPOL=1,即 SCL 空闲状态为高电平,在 CPHA=0,奇数边沿采样的情况下,数据在奇数边沿下降沿要保持稳定并等待采样。 - 串行时钟的偶数边沿上升沿采样
图是 CPOL=1&&CPHA=1 的情形,可以看到未发送数据和发送数据完毕,SCL的状态是高电平,奇数边沿的边沿极性是上升沿,偶数边沿的边沿极性是下降沿。因为 CPHA=1,所以数据在偶数边沿上升沿被采样。在奇数边沿的时候 MOSI 和 MISO 会发生变化,在偶数边沿时候是稳定的。
整合一下,下图为CPHA=0时的 SPI 通讯模式:
下图为CPHA=1时的 SPI 通讯模式:
7.1.4 SPI通讯方式
- 单主单从
两个设备相连,其中一个设备固定作为主机,另外一个固定作为从机。
- 主机设置:SSIG设置为1,MSTR设置为1,固定为主机模式。主机可以使用任意端口连接从机的SS管脚,拉低从机的SS脚即可使能从机
- 从机设置:SSIG设置为0,SS管脚作为从机的片选信号。
单主单从连接配置图如下所示:
2. 互为主从
两个设备相连,主机和从机不固定。
- 设置方法1:两个设备初始化时都设置为SSIG设置为0,MSTR设置为1,且将SS脚设置为双向口模式输出高电平。此时两个设备都是不忽略SS的主机模式。当其中一个设备需要启动传输时,可将自己的SS脚设置为输出模式并输出低电平,拉低对方的SS脚,这样另一个设备就被强行设置为从机模式了。
- 设置方法2:两个设备初始化时都将自己设置成忽略SS的从机模式,即将SSIG设置为1,MSTR设置为0。当其中一个设备需要启动传输时,先检测SS管脚的电平,如果时候高电平,就将自己设置成忽略SS的主模式,即可进行数据传输了。
互为主从连接配置图如下所示:
- 单主多从
多个设备相连,其中一个设备固定作为主机,其他设备固定作为从机。
- 主机设置:SSIG设置为1,MSTR设置为1,固定为主机模式。主机可以使用任意端口分别连接各个从机的SS管脚,拉低其中一个从机的SS脚即可使能相应的从机设备
- 从机设置:SSIG设置为0,SS管脚作为从机的片选信号。
单主多从连接配置图如下所示:
7.2 硬件SPI
以STM32F4为例,先看 SPI 的结构框图,了解它的大致功能
使用 SPI 传输数据的配置步骤:
- SPI 参数初始化(工作模式、数据时钟极性、时钟相位等)
- 使能 SPI 时钟和配置相关引脚的复用功能。
- 使能 SPI,设置 SPI 传输速度
- SPI 传输数据
7.3 软件模拟SPI
与软件模拟IIC一样,用IO模拟SPI的通讯协议,本程序采用mode0的工作模式,来读写w25q
#define MISO RB4
#define MOSI RB2
#define SCK RB5
#define CS RB0
//初始化引脚状态
void init_spi_io(void)
{
CS = 1;
SCK = 0;
MOSI = 0;
}
/*-------------------------------------------------
* 函数名:SPI_RW
* 功能: 主机输出以及输入一个字节
* 输入: data
* 输出: 根据接收的data输出给从机一个字节
--------------------------------------------------*/
unchar SPI_RW(unchar data)
{
unchar i;
for(i=0;i<8;i++)
{
if(data&0x80)
MOSI = 1; //output 'uchar' MSB to MOSI
else
MOSI = 0;
NOP();
data<<=1; //shift next bit into MSB
SCK = 1; //Set SCK high...
NOP();
if(MISO)
data |= 0x01;
else
data &= 0xFE;
NOP();
SCK = 0;
}
return data;
}
/*-------------------------------------------------
* 函数名:WriteEnable
* 功能: 写允许(将WEN置位)
--------------------------------------------------*/
void WriteEnable(void)
{
CS=0;
SPI_RW(0x06);
CS=1;
}
/*-------------------------------------------------
* 函数名:WriteDisable
* 功能: 写禁止(将WEN复位)
--------------------------------------------------*/
void WriteDisable (void)
{
CS=0;
SPI_RW(0x04);
CS=1;
}
/************************************************
// 功能:读取25C64芯片的状态。
// 返回值:状态寄存器数据字节
// 注:25C64内部状态寄存器第0位=0表示空闲,0位=1表示忙。
*************************************************/
unchar SPI_ReadStatus(void)
{
unchar status=0;
CS=0;
SPI_RW(0x05); //0x05读取状态的命令字
status = SPI_RW(0x00);
CS=1; //关闭片选
return status;
}
/************************************************
*程序名:SPI_WriteStatus
*功能: 写25C64芯片的状态寄存器。
* 只有BP1、BP0 (bit7、3、2)可以写、
*注: 25c64内部状态寄存器第0位=0表示空闲,0位=1表示忙。
*************************************************/
void SPI_WriteStatus(unchar Status)
{
CS=0;
SPI_RW(0X01); //0x01读取状态的命令字
SPI_RW(Status); //写入一个字节
CS=1; //关闭片选
}
/*****************************************************************
程序名:SPI_Read
输入: 16位的地址
返回: 读取的数据
说明:从25c64指定的地址读取一个字节
*****************************************************************/
unchar SPI_Read(unint addr)
{
unchar spidata;
while(SPI_ReadStatus()&0x01); //判断是否忙
CS=0; //使能器件
SPI_RW(0x03); //发送读取命令
SPI_RW((unsigned char)((addr)>>8));
SPI_RW((unsigned char)addr);
spidata = SPI_RW(0x00); //读出数据
CS=1;
return spidata;
}
/*****************************************************************
程序名:SPI_Write
输入: 地址,字节数据
说明:将一个字节写入指定的地址
*****************************************************************/
void SPI_Write(unint addr,unchar dat)
{
while(SPI_ReadStatus()&0x01); //判断是否忙
WriteEnable(); //SET WEL
CS=0; //使能器件
SPI_RW(0x02); //发送写命令
SPI_RW((unchar)((addr)>>8));
SPI_RW((unchar)addr);
SPI_RW(dat);
WriteDisable();
CS=1; //关闭片选
while(SPI_ReadStatus()&0x01);
}