1. 面包板

面包板正面面包板背面双面胶撕掉,内部的金属爪金属爪的示意图

把元件的引脚插到面包板的孔里后,它内部的金属爪就会夹住引脚。金属爪的排布规律是,中间的金属爪是竖着放的,上下四排是连在一起的四个整体的金属爪,对应着面包板孔的连接关系。中间竖着的五个孔内部都是连接在一起,因此元件插在一纵排的不同孔位时,内部的金属爪就实现了线路的连接;而上下四排孔整体是连在一起的,这四排是用于供电的,有标正负极,如果我们需要供电,就从上下的孔位中,用跳线引出来即可。

这个供电的引脚,有的面包板不是一整排都是连接的,如果断开需要用跳线把两边连起来

2. 输出设备

2.1 LED 介绍

LED:发光二极管,正向通电点亮,反向通电不亮

LED 电路符号,左边是正极,右边是负极。LED 实物图,引脚没有剪过的 LED,长脚是正极,短脚是负极。LED 内部,较小的一半是正极,较大的一半是负极。

硬件电路:

使用 STM32 的 GPIO 口驱动 LED 的电路:

左边为低电平驱动的电路,LED 正极接 3.3V,负极通过一个限流电阻(面包板为了简化电路省去,设计电路时要注意加上)接到 PA0 上,当 PA0 输出低电平时,LED 两端产生电压差,就会形成正向导通电流,这样 LED 就会点亮了;当 PA0 输出高电平时,因为两端都是 3.3V 电压,不会形成电路,所以高电平 LED 就会熄灭。右边为高电平驱动的电路,LED 负极接 地,正极通过一个限流电阻接到 PA0 上,这时就是高电平点亮,低电平熄灭。

如何选择? 取决于 IO 口高低电平的驱动能力如何了。(推挽输出模式下,高低电平均有较强的输出能力,此时两种接法均可)但是在单片机的电路里,一般倾向使用第一种接法,因为很多单片机或者芯片,都使用了高电平弱驱动,低电平的强驱动的规则,这样可以一定程度上避免高低电平打架。所以高电平驱动能力弱,就不能使用第二种连接方法了。

限流电阻作用:

防止 LED 因为电流过大而烧毁可以调整 LED 的亮度

2.2 蜂鸣器介绍

有源蜂鸣器:内部自带振荡源,将正负极接上直流电压即可持续发声,频率固定无源蜂鸣器:内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整提供振荡脉冲的频率,可发出不同频率的声音

有源蜂鸣器 内部电路图,用了一个三极管开关来进行驱动,在 VCC 和 GND 分别接上正负极的供电,然后中间一个引脚接低电平,蜂鸣器就会响,接高电平,蜂鸣器就关闭。有源蜂鸣器 实物图。

硬件电路:

使用了三极管开关的驱动方案:三极管开关是最简单的驱动电路,对于功率稍微大一点的,直接用 IO 口驱动会导致 STM32 负担过重,此时可以用一个三极管驱动电路来完成驱动的任务。

左边这个图是 PNP 三极管的驱动电路,三极管的左边是 基极,带箭头的是 发射极,剩下的是 集电极。它左边的基极给低电平,三极管就会导通,通过 3.3V 和 GND 就可以给蜂鸣器提供驱动电流了;基极给高电平,三极管截止,蜂鸣器就没有电流。右边这个图是 NPN 三极管的驱动电路,三极管的左边是 基极,带箭头的是 发射极,剩下的是 集电极。它左边的基极给高电平,三极管就会导通,通过 3.3V 和 GND 就可以给蜂鸣器提供驱动电流了;基极给低电平,三极管截止,蜂鸣器就没有电流。

注意:PNP 的三极管最好接在上边,NPN 的三极管最好接在下边。这是因为三极管的通断,是需要在发射极和基极直接产生一定的开启电压的。如果把负载接在发射极这边,可能会导致 三极管不能开启。

2.3 OLED 简介

OLED(Organic Light Emitting Diode):有机发光二极管 OLED显示屏:性能优异的新型显示屏,具有

功耗低:每个像素都是一个单独的发光二极管,每个像素可以自发光的响应速度快:可以使 OLED 有更高的刷新率,总线时序快,可以避免阻塞我们的程序宽视角:因为 OLED 屏幕自发光的、所以在任何角度看,显示内容都是清晰的轻薄柔韧:比如现在手机上的折叠屏、柔性屏等,用的就是 OLED 显示屏等特点

0.96寸OLED模块:小巧玲珑、占用接口少(驱动这个OLED只需要几根线和简单的通信协议)、简单易用,是电子设计中非常常见的显示屏模块(单片机领域金典显示屏);但只有一种颜色,分辨率也很低。

供电:3~5.5V,通信协议:I2C/SPI,分辨率:128*64,外型:4/7 根针脚

硬件电路:

左边是 4 针脚版本的 OLED 电路,GND 接 GND,VCC 接 3.3V,给 OLED 供电。SCL 和 SDA 是 I2C 的通信引脚,需要接在单片机 I2C 通信的引脚上。(驱动函数模块用的是 GPIO 口 模拟的 I2C 通信,所以这两个端口可以接在任意的 GPIO 口。右边是 7 针脚版本的 OLED 电路,GND 接 GND,VCC 接 3.3V,给 OLED 供电。剩下的引脚是 SPI 通信协议的引脚,如果是 GPIO 口模拟的通信协议,也是接在任意的 GPIO 口就行了。

3. 输入设备

3.1 按键介绍

按键:常见的输入设备,按下导通,松手断开

按键实物图:按钮按下去的时候,下面两个引脚是接通的,松手后,两个引脚自动断开。

在单片机中应用按键的一个现象:按键抖动现象

由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动按键没按下是 高电平,按下时 低电平,在按下的瞬间,信号由高电平变为低电平,就会来回抖几下。(抖动比较快,通常在 5~10 ms 之间,人眼分辨不出来;但是对于高速运行的单片机而言,5~10 ms 还是很漫长的)所以我们要对抖动进行过滤,否则就会出现按键按一下,单片机却反映了多次的现象。另外在按键松手的时候,也会有一小段时间的抖动,我们在程序中也要注意过滤一下。最简单的过滤方法就是加一段延时,把这个抖动时间耗过去就没问题了。

硬件电路:

按键的四种接法: 上面两个是 下接按键 的方式(按键按下是低电平,松手是高电平),下面两个是 上接按键 的方式(按键按下是高电平,松手是低电平)。(左边两种接法要求引脚必须是 上拉/下拉输入模式,右边两种接法可以允许引脚是浮空输入的模式,因为已经外置上拉和下拉电阻)

一般用 下接 的方式,原因与 LED 接法类似,是电路设计的习惯和规范

第一种接法是最常用的接法了。将 GPIO 口通过 按键 接地。当按键按下时,PA0 直接被下拉到 GND,此时读取 PA0 口的电压就是低电平;当按键松手时,PA0 被悬空,引脚电压不确定。所以在这种解法下,必须要求 PA0 是上拉输入的模式,否则就会出现引脚电压不确定的错误现象。(如果 PA0 是上拉输入模式,引脚再悬空,就是高电平。所以这种方式下,按下按键,引脚为低电平,松手,引脚为高电平。第二种接法相比较第一种接法再外部接了一个上拉电阻。当按键松手时,引脚由于上拉作用,自然保持为高电平;当按键按下时,引脚直接接到 GND,引脚为低电平。这种状态下,引脚不会出现悬空状态,所以此时 PA0 引脚可以配置为 浮空输入或者上拉输入,如果是上拉输入,就是内外两个上拉电阻共同作用,这时高电平会更强一下,对应高电平更加稳定;但当引脚被强行拉到低时,损耗也会大一些。第三种接法,PA0 通过按键接到 3.3V。要求 PA0 必须配置为 下拉输入模式,当按键按下时,引脚为高电平,松手时,引脚回到默认值低电平。

要求单片机的引脚可以配置为 下拉输入模式,一般单片机可能不一定有下拉输入模式,最好还是用上面的接法。

第四种接法,在刚才的这种接法下面再外接一个下拉电阻。PA0 需要配置为 下拉输入模式或者浮空输入模式。

3.2 传感器模块介绍

传感器模块工作原理:利用传感器元件(光敏电阻/热敏电阻/红外接收管等)的电阻会随外界模拟量的变化而变化,但是电阻的变化不容易直接被观察,所以通常将传感器元件与定值电阻进行串联分压即可得到模拟电压输出,对于电路来说,检测电压非常容易,再通过电压比较器对模拟电压进行二值化即可得到数字电压输出。

光敏电阻传感器:光线越强,光敏电阻(N1)阻值越小热敏电阻传感器:温度越高,热敏电阻(N1)阻值越小对射式红外传感器:红外光线越强,红外接收管(N1)阻值越小反射式红外传感器:向下发射红外光,然后检测反射光,红外光线越强,红外接收管(N1)阻值越小(可用作寻迹小车)硬件电路:N1 就是传感器元件所代表的可变电阻。它的阻值可以根据环境的光线、温度等模拟量进行变化。R1 是跟 N1 进行分压的定值电阻。R1 和 N1 串联,一端接在 VCC 正极,一端接在 GND 负极,构成了基本的分压电路,C2 是一个滤波电容,是为了给中间的电压输出进行滤波的,用来滤除一些干扰,保证输出电压波形的平滑。

一般我们遇到一端接在电路中,一端接地的电容都可以考虑一下是不是滤波电容。如果是滤波电容,那这个电容就是用来保证电路稳定的,并不是电路的主要框架,我们在分析电路的时候,可以先把这个电容给抹掉,使我们的电路分析更加简单。

采用分压定理分析传感器电阻的阻值变化对输出电压的影响,也可以用上下拉电阻的思维(阻值越小,拉力越强)来分析。 当 N1 阻值变小时,下拉作用就会增强,中间的 AO 端电压就会拉低,极端情况下, N1 阻值为 0,AO 输出被完全下拉,输出 0V; 当 N1 阻值变大时,下拉作用就会减弱,中间的 AO 端由于 R1 的上拉作用电压就会升高,极端情况下, N1 阻值为 无穷大,相当于断路,输出 电压被 R1 拉高至 VCC。

在单片机电路中出现的的弱上拉、弱下拉、强上拉、强下拉中的强和弱就指电阻阻值的大小。上拉和下拉就指接到 VCC 还是 GND。

在这两个电阻的分压下,AO 就是我们想要的模拟电压输出了,所以 AO 电压就直接通过排针输出了,这就是 AO 电压的由来。

数字输出就是对 AO 进行二值化的输出,二值化通过芯片 LM393 来完成的,是一个电压比较器芯片,里面有两个独立的电压比较器电路,剩下的是 VCC 和 GND 供电。VCC 接到了电路的 VCC,GND 接到了电路的 GND,这里有个电容是电源供电的滤波电容。电压比较器其实就是一个运算放大器。

运算放大器当作比较器情况:当同相输入端的电压大于反相输入端的电压时,输出就会瞬间升为最大值,也就是输出接 VCC;反之当同相输入端的电压小于反相输入端的电压时,输出就会瞬间降为最小值也就是输出接 GND。这样就可以对一个模拟电压进行二值化了。

这里同相输入端 IN+ 接到了 AO,就是模拟电压端,IN- 接了一个电位器,电位器接法也是分压电阻的原理,拧动电位器,IN- 就会生成一个可调的阈值电压,两个电压进行比较,最终输出结果是 DO,数字电压输出,DO 最终就接到了 引脚的输出端,这就是 数字电压 的由来。

右边有两个指示灯电路。左边是电源指示灯,通电就亮;右边是 DO 输出指示灯,可以指示 DO 的输出电平,低电平电路,高电平熄灭。

右边 DO 这里多了个 R5 上拉电阻,是为了保证默认输出为 高电平的。

P1 的排针,分别是 VCC、GND、DO 和 AO。

红外传感器 会多一个点亮红外发射管的电路,发射管发射红外光,接收管接收红外光。

模拟电压表示接收光的强度。电位器直接换成两个电阻进行分压,数字输出就是固定阈值的二值化了。模块通常用来检测通断,所以阈值不需要过多的调整。

硬件电路:

使用模块的方案,VCC 接 3.3V,GND 接 GND,用于供电。DO 数字输出随便接一个端口,用于读取数字量。AO 模拟输出,暂时不接。

4. 配合外部中断的设备

使用外部中断模块的特性:对于 STM32 来说,想要获取的信号是外部驱动的很快的突发信号。

比如旋转编码器的输出信号,我们可能很久都不会拧它,此时不需要 STM32 做任何事,但是一拧它,就会有很多脉冲波形需要 STM32 接收。这个信号是突发的,STM32 不知道什么时候会来;同时它还是外部驱动的,STM32 只能被动读取;最后这个信号非常快,STM32 稍微晚一点来读取,就会错过很多的波形。这种情况就可以考虑使用 STM32 的外部中断。比如红外遥控接收头的输出,接收到遥控数据之后,会输出一段波形,这个波形转瞬即逝,并且不会等你,所以就需要我们用外部中断来读取。按键虽然也是外部驱动的突发事件,但不推荐用外部中断来读取按键,因为外部中断不好处理按键抖动和松手检测的问题。对于按键来说,他的输出波形也不是转瞬即逝的。所以要求不高的话可以在主程序中循环读取。如果不想用主程序循环读取,可以考虑一下定时器中断读取的方式。(既可以做到后台读取按键值、不阻塞主程序;也可以很好的处理按键抖动和松手检测的问题)

4.1 旋转编码器

作用及原理:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向

类型:机械触点式/霍尔传感器式/光栅式

工作原理:

第一种:只有一个光栅加红外对管的编码器,最简单的编码器样式,使用对射式红外传感器来测速的,配合一个光栅编码盘。当这个编码盘转动时,红外传感器的红外光就会出现遮挡、透过、遮挡、透过这样的现象,对应模块输出的电平就是高低电平交替的方波,方波的个数代表转过的角度,频率代表转速,用外部中断来捕获方波的边沿,以此判断位置和速度。但是这个模块只有一路输出,正转反转输出波形没法区分,所以这种测速方法只能测位置和速度,不能测旋转方向。

这只能输出一个方波信号,并不是正交编码器。

第二种:内部用金属触点来进行通断的,是一种机械触点式编码器,左右是两部分开关触点,其中内侧两根细的触点跟中间引脚连接,外侧触点,左边接在 左引脚,右边接在 右引脚,这就是这些触电的连接方式。中间圆的金属片是一个按键,旋转编码器的轴可以按下去的,这个按键的两根线,就在上面引出来了。按键的轴按下,上面两根线短路,松手,上面两根线断开,就是个普通按键。编码盘也是一系列像光栅一样的东西,只不过是金属触点,在旋转时以此接通和断开两边的触点,这个金属盘的位置是经过设计的,能让两侧触点的通断产生 90 度的相位差,最终配合外部电路。编码器的两个输出就会输出相位相差 90 度的波形,根据 A 相和 B 相 方波是 提前还是滞后90度 来区分是 反转还是正转,这种相位相差 90 度的波形就叫做正交波形,带正交波形信号输出的编码器,是可以用来测方向的。这就是单向输出和两相正交输出的区别。(还有一个引脚输出方波信号代表转速,另一个输出高低电平代表旋转方向的方式)里面靠两个触点交替导通,可以输出 A 相和 B 相两个正交信号,是正交编码器。

这种编码器一般用于进行调节,比如音响调节音量这样的用途,因为是触点接触的形式,不适合电机这种高速旋转的地方。另外几种都是非接触的形式,可以用于电机测速。(电机测速在电机驱动中的应用还是非常常见的)

第三种:直接附在电机后面的编码器。是霍尔传感器形式的编码器,中间是一个圆形磁铁,边上有两个位置错开的霍尔传感器。当磁铁旋转时带动中间的磁铁旋转,两个霍尔传感器 90° 放置,通过霍尔传感器,就可以输出 A 相和 B 相 两个正交的方波信号,是正交编码器。接口的引脚有六根线,最左和最右一般直接接到电机的,然后是靠里一些的两根是编码器电源,最中间的两根就是 A 相和 B 相的输出了。 第四种:独立的编码器元件,输入轴转动时,输出就会有波形,也可以测速和测方向,一般都是正交编码器,当然也有的不是,具体用法看相应手册。接口的引脚一般有六个,两个是编码器电源,两个是 A、B 相,一般还有一个编码器 0 位置的输出,也就是 z 相,0 位置就是编码器每转到一个固定位置的时候,输出一个脉冲,一般应用于位置测量,校准 0 位置用的。最后还有一个引脚,一般是空脚,没有用到。

硬件电路: 中间是旋转编码器,上面按键的两根线并没有使用,是悬空的,下面的就是编码器内部的两个触点了,旋转轴旋转时,这两个触点以相位相差 90 度的方式交替导通,因为这只是个开关信号,所以要配合外围电路才能输出高电平;左边接了一个 10k 的上拉电阻,默认没旋转的情况下,被上拉为高电平,通过 R3 这个电阻输出到 A 端口的就是高电平,当旋转时,内部触点导通,被直接拉低到 GND 了,再通过 R3 输出,A 端口就是低电平了。R3 是一个输出限流电阻,是为了防止模块引脚电流过大的;C1 是输出滤波电容,防止一些输出信号抖动的;右边电路和左边一模一样。最后中间下边这个模块的 C 端口就直接接到了 GND。

右边使用这个模块时,上边 VCC、GND 接电源,下面的 A 相输出和 B 相输出接到 STM32 的两个引脚上。比如 PB0 和 PB1,注意引脚的 GPIO_Pin 编号不要一样就行了。然后中间的 C 引脚,就是 GND,暂时不用。

5. 配合 PWM 信号输入的设备

5.1 舵机简介

舵机是一种根据 输入 PWM 信号占空比 来控制舵机输出轴的角度的装置(PWM 波形当作一个通信协议来使用的,这也是一种常用的应用形式,与用 PWM 等效一个模拟输出,关系不大)

型号:SG90 三根输入线:两根电源线、一根信号线,PWM 通过输入到信号线来控制舵机 白色的输出轴:固定在指定的角度不动,固定的位置由信号线的 PWM 信号来决定的 拆解结构:舵机并不是一种单独的电机,内部由直流电机驱动,还有一个控制电路板,是一个电机的控制系统。大概执行逻辑:PWM 信号输入到控制板,给控制板指定一个目标角度,然后电位器检测输出轴的当前角度,如果大于目标角度,电机就会反转,如果小于目标角度,电机就会正转,最终使输出轴固定在指定角度,这就是舵机的内部工作流程。 输入 PWM 信号要求:输入信号脉冲宽度 / 周期为 20 ms,高电平宽度为 0.5 ms~2.5 ms(舵机输出轴转角 -90°~90°,对应关系都是线性分配的,按比例来),占空比是这个范围。 实际应用:机器人,机械臂可以用舵机来控制关节,遥控车,遥控船可以用舵机来控制方向。还有一些其他结构,都可以考虑使用舵机。

硬件电路: 引脚定义图:

在舵机上有三根线,分别是黑、红、黄:对应的,黑色是电源负极 GND,红色是电源正极,这里的 5V 舵机就接 +5V,黄色是信号线,接 PWM 信号。本实验的舵机上三根线分别是棕红橙,对应的是棕色是电源负,红色是电源正,橙色是信号线。

引脚电路图:

GND 接 GND,电源的 +5V,这个是电机的驱动电源,一般电机都是大功率设备,它的驱动电源也必须是一个大功率的输出设备,如果能像这样单独提供供电,就再好不过了。如果不能,那也要注意电源的功率是不是能达标。如果单独供电的话,供电的负极要和 STM32 共地,然后正极接在 5V 供电引脚上,对于我们套件的话,可以直接从 STLINK 的 5V 输出脚,引一根线,接到这里,这也就是使用 USB 的 5V 供电,也是可以带的动的。信号线直接接到 STM32 引脚上就行,比如 PA0,因为这个舵机内部是有驱动电路的,所以单片机引脚可以直接接到这里来,我们这个 PWM 只是一个通信线,是不需要大功率的。

5.2 直流电机及驱动简介

直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转。直流电机是一个单独的电机,里面没有驱动电路,所以要外挂一个驱动电路来控制。

直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作,可以用 PWM 来控制电机的速度。

电机这类器件基本上都属于大功率设备。必须要加驱动电路才能控制,电机驱动电路也是一个研究课题,现在市面上也有很多驱动电路可以选择。比如 TB6612、DRV8833、L9110、L298N 等等,这些都是比较常见的电机驱动芯片,另外还有一些用分离元件 MOS 管搭建的电路,这个功率可以做的更大一些。

TB6612 是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向。

芯片里有两路驱动电路的,可以独立的控制两个电机;H桥型驱动电路,里面一路有四个开关管,所以就可以控制正反转。

像有些芯片,比如 ULN2003,它里面一路就只有一个开关管,所以它就只能控制电机在一个方向转

使用现成的芯片还是挺方便的,电路已经设计好并集成在芯片里了,我们直接拿来用就行。

型号:130直流电机,有两个引脚。 电机驱动板:

上面的芯片就是 TB6612,外围电路就只需要三个滤波电容就行了,可见芯片的集成度还是非常高的

H 桥电路的基本结构: 由两路推挽电路组成的,如果左边上管导通,下管断开,那左边输出就是接在VM的电机电源正极;上管断开,下管导通,那左边输出就是接在 PGND 的电源负极。如果有两路推挽电路,中间这里接一个电机,左上和右下导通,那电流就是从左上边流向右下边;右上和左下导通,那电流方向就反过来了,从右上边流向左下边;H桥可以控制电流流过的方向,所以它就能控制电机的正反转,这就是电机驱动芯片内部的部分电路。

硬件电路:

VM:电机电源的正极,跟舵机的电源要求一样,要接一个可以输出大电流的电源。电压与电机的额定电压保持一致。VCC:逻辑电平输入端,电压要和控制器的电源保持一致。比如使用 STM32,是 3.3V 器件,那就接 3.3V;如果是 51 单片机,是 5V 的器件,那就接 5V,这个引脚并不需要大功率,所以可以和控制器共有一个电源。三个GND:一样的引脚,在板子内部都是连通的,随便选一个 GND 接 系统负极即可AO1、AO2、BO1、BO2 就是两路电机的输出了,可以像图示分别接两个电机,AO1 和 AO2 就是 A 路的两个输出,它的控制端就是上面这三个 PWMA、AIN2 和 AIN1,这三个引脚控制下面 A 路的一个电机,对应于灰色填充部分,这三个引脚直接接到单片机的 GPIO 口就行了;其中 PWMA 引脚要接 PWM 信号输出端,其他两个引脚可以任意接两个普通的 GPIO 口,那这三个引脚给一个低功率的控制信号,驱动电路就会从 VM 汲取电流,来输出到电机上,这样就可以完成低功率的控制信号控制大功率设备的目的。右边这一路也是一样,两路的功能和操作方法是完全一样的。STBY(Stand By)引脚:待机控制脚,如果接 GND,芯片就不工作,处于待机状态;如果接逻辑电源 VCC,芯片就正常工作,这个引脚如果不需要待机模式的话,可以直接接 VCC,3.3V,如果需要的话,可以任意接一个 GPIO,给高低电平就可以控制了。

三个脚是如何控制电机正反转和速度的呢?取决于右下方表格。 这里输入是 IN1、IN2、PWM 和 STBY,STBY 低电平就待机,高电平就正常工作,然后右边是输出,O1、O2 和模式状态。

如果 IN1 和 IN2 全都接高电平,两个输出就都为低电平,这样两个输出没有电压差,电机是不会转的如果 IN1 和 IN2 全都接低电平,输出直接关闭,这样电机也是不会转的如果 IN1 接低电平,IN2 接高电平,电机就是处于反转状态,转不转取决于 PWM,如果 PWM 给高电平,输出就是一低一高,有电压差了,电机可以转,这时候定义的是反转,开始转了;如果PWM 给低电平,输出两个低电平,电机还是不转,这就是反转的逻辑。IN1 给低,IN2 给高,PWM 高转低不转,如果 PWM 是个不断翻转的电平信号,那电机不就是快速的反转、停止、反转、停止,如果 PWM 频率足够快,那电机就可以连续稳定的反转了,并且速度取决于 PWM 信号的占空比,这就是反转的工作流程。在这里就是使用 PWM 来等效一个模拟量的功能。IN1 给高,IN2 给低,这就是电机的正转状态,PWM 高,正转,低,停止,如果 PWM 频率足够快,那电机就是连续稳定的正转了,并且速度取决于 PWM 信号的占空比。

6. 配合 I2C 通信的外设

在《I2C 通信-stm32入门》节介绍了 I2C 通信的协议标准,有了 I2C 通信,我们就可以实现指定地址写和指定地址读的逻辑,这样,即使这个外挂芯片的各种寄存器不在 STM32 内部,我们仍然可以通过通信协议,实现读写外挂芯片寄存器的功能,这样就能完全掌控这个外挂芯片了。

要想研究清楚芯片的功能,看它对应的数据手册是必不可少的。

PS(产品说明书):介绍芯片的功能描述、电气参数、引脚定义、硬件电路等等的介绍。RM(寄存器映像):上面有个总表,就是这个芯片内部所有的寄存器以及它们的地址。然后剩下的内容就是这个总表中每个寄存器以及寄存器中每一位的详细解释。相当于 STM32 参考手册里面的寄存器描述。

当我们写代码实际操作硬件的时候,寄存器描述就是必不可少的,因为寄存器描述里都是实实在在的实现细节。而当我们需要了解芯片的原理、参数和硬件电路的时候呢,就需要参考这个产品说明书了,这里面讲的是芯片的功能概括,以及电路的大体工作流程。 要想学好这个芯片,这两个手册都是要从头到尾看一遍的。当然这两个文档都是英文的,如果英文不好的话,可以借助翻译软件的划词翻译功能来辅助理解。

6.1 MPU6050 简介

MPU6050是一个 6 轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景

在现实的三维空间里,只有 X、Y、Z 三个轴,但是这个 MPU6050 芯片里面,有加速度计和陀螺仪两种传感器,可以分别测量 XYZ 3 个轴的加速度和角速度,加起来总共就是 6 个轴,所以这个芯片是 6 轴姿态传感器。当然如果芯片里再集成一个 3 轴的磁场传感器,测量 XYZ 轴的磁场强度,那就叫做 9 轴姿态传感器;如果再集成一个气压传感器,测量气压大小,那就叫做 10 轴姿态传感器,一般气压值反映的是高度信息,海拔越高,气压越低,所以气压计是单独测量垂直地面的高度信息的,这些就是姿态传感器的一些术语。之后别人再说这个传感器是 6 轴、9 轴、10 轴,你就知道了: 6 轴 就是 3 轴加速度和 3 轴角速度 9 轴 就是 3 轴加速度和 3 轴角速度和 3 轴磁场强度 10 轴 就是 3 轴加速度和 3 轴角速度和 3 轴磁场强度和 1 个气压强度 这个大家了解一下。

那要这么多轴的信息,是要干啥呢? 答案是:通过数据融合,可进一步得到姿态角,或者叫做欧拉角,这个欧拉角是什么呢?以飞机为例,欧拉角就是飞机机身相对于初始 3 个轴的夹角,飞机机头下倾或者上仰,这个轴的夹角叫做俯仰,Pitch;飞机机身左翻滚或者右翻滚,这个轴的夹角叫做滚转,Roll;飞机机身保持水平,机头向左转向或者向右转向,这个轴的夹角叫做偏航,Yaw,简单来说,欧拉角就表达了飞机此时的姿态。飞机是上仰了还是下倾了,飞机向左倾斜还是向右倾斜,通过欧拉角都能清晰地表示出来。如果你想做一个飞控算法,为了保持飞机姿态平稳,那么得到一个精确且稳定的欧拉角就至关重要。但是可惜的是,之前我们所说的加速度计、陀螺仪、磁力计,任何一种传感器都不能获得精确且稳定的欧拉角,要想获得精确且稳定的欧拉角,就必须进行数据融合,把这几种传感器的数据结合起来,综合多种传感器的数据,取长补短,这样才能获得精确且稳定的欧拉角。 常见的数据融合算法,一般有互补滤波、卡尔曼滤波等,这就涉及到惯性导航领域里,姿态解算的知识点了,不过我们本节的侧重点是 I2C 通信,我们最终的程序现象就是把这些传感器的原始数据读出来,显示在 OLED 上,就完事了,所以有关姿态解算的内容暂时不会涉及。 姿态传感器解算出姿态角之后,就常应用于平衡车、飞行器等需要检测自身姿态的场景。平衡车呢,如果传感器检测到车身向前或者向后倾斜,程序就可以控制轮子进行调整,保持平衡车的平衡。飞行器呢,这个控制的轴就多一些,一般至少需要检测俯仰角和滚转角两个夹角,然后控制电机保持飞机的平衡,这就是这个 MPU6050 姿态传感器的作用。

MPU 6050 内部的 6 轴传感器的作用:

3 轴加速度计(Accelerometer,一般简称 Accel,或者 Acc,或者 A)作用:测量 X、Y、Z 轴的加速度。

这个英文简称大概记一下,之后会经常出现。

XYZ 轴就是一个三维的坐标系,其中横向的这个轴定义为 X 轴,纵向的这个轴定义为 Y 轴,垂直于芯片的这个轴定义为 Z 轴,这就是这个芯片对 XYZ 轴的定义,然后在 XYZ 轴,这个芯片内部都分别布置了一个加速度计。 什么是加速度计呢? 这就是加速度计的结构图,其中水平的这个虚线是感应轴线,中间是有一个具有一定质量、可以左右滑动的小滑块,然后左右各有一个弹簧顶着它。可以想象得到,如果你把这个东西拿在手上,来回晃,中间这个小滑块就会左右移动,去压缩或拉伸两边的弹簧。当滑块移动时,就会带动上面的电位器滑动,这个电位器其实就是一个分压电阻,然后我们测量电位器输出的电压,就能得到小滑块所受的加速度值了。 其实可以发现,这个加速度计实际上就是一个弹簧测力计。根据牛顿第二定律,F = ma,我们想测量这个加速度 a,就可以找一个单位质量的物体,测量它所受的力 F,就行了。所以你听这个名字,加速度计,感觉很高大上,很难理解,实际上它就是一个测力计而已。这就好理解了吧。 那在这个芯片里面,XYZ,3 个轴,分别都有这样一个测力计,这里可以用一个模型来辅助理解,你就想象这个芯片里面,有 6 个测力的秤,组成一个正方体,然后正方体内部,放置一个大小正好的单位质量小球,这个小球压在了这个秤面上,就会产生对应一个轴的数据输出,比如小球压在下面这个面,就是 Z 轴的正值,小球压上面这个面,就是 Z 轴的负值。对向的两个面一组,一面为正值,一面为负值,那正方体 6 个面所测的力就是 3 个轴的加速度值,这就是加速度计的一个直观理解。比如你把芯片静置水平放在地球上,那就只有底面的测力计受到小球的压力,所以此时数据输出就是 XY 轴输出为 0,Z 轴输出 1 个 g 的加速度值,如果此时芯片处于自由落体,那就是所有面都不受力,此时数据输出就是 XYZ 轴都为 0,如果此时芯片向左倾斜放置,那就是底面和左面都受力,这时求一个三角函数就能得到向左的倾角了,不过这个倾角只有芯片静置的时候才是正确的,因为加速度分为重力加速度和运动加速度,如果此时芯片运动起来了,这个三角函数求得的倾角就会受运动加速度的影响。举个例子,比如你坐在汽车里,现在汽车突然向前加速,你是不是感觉椅子底面和靠背都受力,这时如果用三角函数求角度,得到的结果就是你的车停在一个斜坡上,停在斜坡上,也是椅子底面和靠背都受力,对吧。但实际上车的状态却是水平向前加速的,所以仅使用加速度计求角度,只能在物体静止的时候使用,当物体运动起来了,这个角度都会受运动加速度的影响而变得不准确,这就是加速度计的测量原理和特性。 总结一下就是:加速度计具有静态稳定性,不具有动态稳定性。

3 轴陀螺仪传感器(Gyroscope,简称 Gyro,或者 G)作用:测量 X、Y、Z 轴的角速度

这个英文简称大概记一下,之后会经常出现。

这就是陀螺仪的机械模型,中间是一个有一定质量的旋转轮,外面是 3 个轴的平衡环,当中间这个旋转轮高速旋转时,根据角动量守恒的原理,这个旋转轮具有保持它原有角动量的趋势,这个趋势可以保持旋转轴方向不变,当你外部物体的方向转动时,内部的旋转轴方向并不会转动,这就会在平衡环连接处产生角度偏差,如果我们在连接处放一个旋转的电位器,测量电位器的电压。就能得到旋转的角度了,从这里分析,陀螺仪应该是可以直接得到角度的,但是我们这个 MPU6050 的陀螺仪,并不能直接测量角度,可能是结构的差异或者工艺的限制。我们这个芯片内部的陀螺仪,测量的实际上是角速度,而不是角度,这个注意一下。陀螺仪测量 XYZ 轴的角速度值,分别表示了,此时芯片绕 X 轴、绕 Y 轴和绕 Z 轴旋转的角速度。这里也可以用一个模型来辅助理解,这个测量角速度的陀螺仪,你可以把它想象成是游乐园的旋转飞椅,中间的轴转的越快,这个椅子飞的就越远,最终我们测量一下对向两个椅子飞起来的距离,或者飞起来的夹角,就能得到中间轴的加速度了。这就是测量角速度的陀螺仪工作原理。 那如果我们想通过角速度得到角度的话,我们只需要对角速度进行积分即可,角速度积分,就是角度,和加速度计测角度一样,这个角速度积分得到的角度也有局限性,就是当物体静止时。角速度值会因为噪声无法完全归零,然后经过积分的不断累积,这个小噪声就会导致计算出来的角度产生缓慢的漂移,也就是角速度积分得到的角度经不过时间的考验。不过这个角度,无论是运动还是静止,都是没问题的,它不会受物体运动的影响,所以总结下来就是,陀螺仪具有动态稳定性,不具有静态稳定性,这个陀螺仪是动态稳定,静态不稳定,之前加速度计是静态稳定,动态不稳定。这两种传感器的特性正好互补,所以我们取长补短,进行一下互补滤波,就能融合得到静态和动态都稳定的姿态角了。这就是姿态解算的大体思路,当然实际的姿态解算肯定会更加复杂一些,我们之后有机会再研究这个问题。

那这个加速度计和陀螺仪的结构原理,就介绍到这里。

接下来我们来看几个 MPU6050 的重要参数:

其他参数参考手册进行分析

16位ADC采集传感器的模拟信号,量化范围:-32768~32767

之前我们也了解过,这里无论是加速度计还是陀螺仪,他们的基本原理都是,设计一种装置,当传感器所感应的参数变化时,这个装置能带动电位器滑动,或者装置本身的电阻,可以随感应参数变化而变化,这样再外接一个电源,通过电阻分压,就能把现实世界的各种状态用电压表示出来了。当然这个传感器里面肯定不是图片这里这样的机械结构,芯片里面都是通过电子的技术来完成各种参数的测量的,要不然也塞不到这么小的芯片里,我们理解的时候,可以把它想象成这种机械结构,但实际上芯片里面,如何用电来完成相同的功能,这就是这个厂家的秘籍了,我们也不用管。总之电子的传感器,最终也是输出一个随姿态变化而变化的电压。要想量化这个电压信号,那就离不开 AD 转换器了。所以这个芯片内部也是自带了 AD 转换器,可以对各个模拟参量进行量化,这个 ADC 是 16 位的,那量化输出的数据变化范围就是 216。如果作为无符号数的话,就是 0~65535,这里因为传感器每个轴都有正负的数据,所以这个输出结果是一个有符号数,量化范围是:-32768~32767,数据是 16 位的,会分为 2 个字节存储。这个之后我们读取数据寄存器的时候就可以看到了。

加速度计满量程选择:±2、±4、±8、±16(g) 陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)

这个 16 位有符号数的范围,它所对应的物理参量范围是多少呢,这里就需要定义一个满量程范围,这个满量程范围,就相当于我们之前学 ADC 的时候,那个 VREF 参考电压一样,你 AD 值达到最大时,对应电压是 3.3V,还是 5V 啊,需要有一个参考电压来指定,这里也是一样。16 位 AD 值达到最大时对应的物理参量具体是多少,也是由满量程范围来决定的。 在这里,加速度计和陀螺仪的满量程范围都有几个选项可以选择。 加速度计满量程,可以选择 ±2、±4、±8、±16 单位是 g,也就是重力加速度,1 个 g = 9.8 m/s2。 陀螺仪满量程,可以选择±250、±500、±1000、±2000 单位是 °/sec,就是角速度的单位,每秒旋转了多少度。这里呢,如果你所测量的物体运动非常剧烈,就可以把满量程选的大一些,防止你的加速度或者角速度超出了量程;如果你所测量的物体运动比较平缓,就可以选择比较小的量程,这样测量的分辨率就会更大。 举个例子: 比如你选择加速度计满量程为 ±16g,当读取 AD 值为最大值 32768 时,当然实际的最大值是 32767,那就表示此时测量的加速度为满量程 16g,AD 值为 32768 的一半时,就表示加速度为 8g;如果选择满量程为 ±2g 的话,那此时 32768 就对应 2g 的加速度,32768 的一半就对应 1g 的加速度。因为 AD 值的范围是一定的,所以满量程选择越小,测量就会越细腻;另外 AD 值和加速度值是线性关系、一一对应的,由 AD 值求加速度,就是乘一个系数就可以了,这跟我们之前学 ADC 的时候,由 AD 值求电压值,是一样的道理。 然后下面陀螺仪的满量程选择,也是同样的操作,满量程选的越大,测量范围就越广;满量程选的越小,测量分辨率越高。 所以这个满量程的选择,要根据实际需求来。

可配置的数字低通滤波器

在这个芯片里,可以配置寄存器来选择对输出数据进行低通滤波;如果你觉得输出数据抖动太厉害,就可以加一点低通滤波,这样输出数据就会平缓一些。

可配置的时钟源 可配置的采样分频

这两个参数是配合使用的,时钟源经过这个分频器的分频,可以为 AD 转换和内部其他电路提供时钟,控制分频系数,就可以控制 AD 转换的快慢了。

这些就是这个芯片的大概的参数。至于这些可配置的具体值,参考手册时再介绍。

I2C从机地址:1101000(AD0=0) 1101001(AD0=1)

下面这还有一点要提一下,就是这个芯片进行 I2C 通信的从机地址,这个可以在手册里查到,当 AD0 = 0 时,地址为 1101 000;当 AD0 = 1 时,地址为 1101 001。AD0 就是板子引出来的一个引脚,可以调节 I2C 从机地址的最低位,这里地址是 7 位的;

如果像这样,用二进制来表示的话,一般没啥问题,如果在程序中,用 16 进制表示的话,一般会有两种表示方式,以这个 1101 000 的地址为例,第一种,就是单纯地把这 7 位二进制数转换为 16 进制,这里 110 1000,低 4 位和高 3 位切开,转换 16 进制,就是 0x68;所以有的地方就说 MPU6050 的从机地址是 0x68. 然后我们看一下之前 I2C 通信的时序,这里第一个字节的高 7 位是从机地址,最低位是读写位,所以如果你认为 0x68 是从机地址的话,在发送第一个字节时,要先把 0x68 左移 1 位,再按位或上读写位,读 1 写 0,这是认为从机地址是 0x68 的操作。当然目前还有另一种常见的表示方式,就是把 0x68 左移 1 位后的数据,当作从机地址,0x68 左移一位之后,是 0xD0,那这样,MPU6050 的从机地址就是 0xD0,这时,在实际发送第一个字节时,如果你要写,就直接把 0xD0 当作第一个字节,如果你要读,就把 0xD0 或上 0x01,即 0xD1 当作第一个字节,这种表示方式,就不需要进行左移操作了,或者说这种表示方式,是把读写位也融入到从机地址里了。 0xD0 是写地址,0xD1 是读地址,这样表示的,所以你之后看到有地方说 0xD0 是 MPU6050 的从机地址,那它就是融入了读写位的从机地址;如果你看到有地方说 0x68 是 MPU6050 的从机地址,这也不要奇怪,这种方式就是直接把 7 位地址转换 16 进制得到的。在实际发送第一个字节时,不要忘了先左移 1 位,再或上读写位,这就是两种从机地址的表示方式。 当然无论哪种表示方式,得到的 I2C 第一个字节都是一样的,在实际情况中,两种方式都有出现过。我个人比较喜欢融入读写位的这种表达方式,所以在我的程序中,MPU6050 的从机地址是 0xD0,这个大家了解一下

好,那这里参数就看到这里。我们接着继续来看一下硬件电路: 上边展示的就是 MPU6050 模块的原理图,其中右边是 MPU6050 的芯片,左下角是一个 8 针的排针,左上角是一个 LDO,低压差线性稳压器。我们来看一下:

右边这个 MPU6050 芯片,芯片本身 的引脚是非常多的,包括时钟、I2C 通信引脚、供电、帧同步等等,不过这里有很多引脚我们都用不到。还有一些引脚,是这个芯片最小系统里的固定连接,这个最小系统电路,一般手册里都会有,抄过来就行啊。然后看左下角,引出来的引脚,有 VCC 和 GND,这两个引脚是电源供电,然后 SCL 和 SDA,这两个引脚是 I2C 通信的引脚。在右上角可以看到,SCL 和 SDA,模块已经内置了两个 4.7k 的上拉电阻了。所以在我们接线的时候,直接把 SCL 和 SDA 接在 GPIO 口就行了,不需要再在外面另外接上拉电阻了。接着下面,还有 XCL 和 XDA,这两个是芯片里面的主机 I2C 通信引脚,设计这两个引脚是为了扩展芯片功能。之前我们说过,MPU6050 是一个 6 轴姿态传感器,但是只有加速度计和陀螺仪的 6 个轴,融合出来的姿态角是有缺陷的,这个缺陷就是,绕 Z 轴的角度,也就是偏航角,它的漂移无法通过加速度计进行纠正。所以根据项目要求,这个 6 轴传感器可能不够用,需要进行扩展。那这个时候,这个 XCL 和 XDA 就可以起作用了,XCL 和 XDA,通常就是用于外接磁力计或者气压计,当接上磁力计或者气压计之后,MPU6050 的 主机接口 可以直接访问这些扩展芯片的数据,把这些扩展芯片的数据读取到 MPU6050 里面。在 MPU6050 里面会有 DMP 单元,进行数据融合和姿态解算,当然如果你不需要 MPU6050 的解算功能的话,也可以把这个磁力计或者气压计,直接挂载在 SCL 和 SDA 这条总线上。因为 I2C 本来就可以挂载多设备,所以把多个设备都挂载在一起也是没问题的。这就是 XCL 和 XDA 的用途。然后下面 AD0 引脚,这个之前说过,它是从机地址的最低位,接低电平的话,7 位从机地址就是 1101 000;接高电平的话,7 位从机地址就是 1101 001。这里电路中可以看到,有一个电阻,默认弱下拉到低电平了,所以引脚悬空的话,就是低电平,如果想接高电平,可以把 AD0 直接引到 VCC,强上拉至高电平。最后一个引脚是 INT,也就是中断输出引脚,可以配置芯片内部的一些事件,来触发中断引脚的输出,比如数据准备好了、I2C 主机错误等,另外芯片内部还内置了一些实用的小功能,比如自由落体检测、运动检测、零运动检测等。,这些信号都可以出发 INT 引脚产生电平跳变,需要的话可以进行中断信号的配置。当然如果不需要的话,那就可以不配置,这个引脚也不需要用。那这就是这些引脚的功能描述。然后看一下左上角的这个 LDO。这部分是供电的逻辑,在手册里可以查到,这个 MPU6050 芯片的 VDD 供电是 2.375~3.46 V,属于 3.3V 供电的设备,不能直接接 5V。所以为了扩大供电范围,这个模块的设计者就加了个 3.3V 的稳压器,输入端电压 VCC_5V 可以在 3.3V 到 5V 之间,然后经过 3.3V 的稳压器,输出稳定的 3.3V 电压,给芯片端供电。后面的 R201 和 LED 是电源指示灯,只要 3.3V 端有电,电源指示灯就会亮,所以这一块需不需要,可以根据你的项目要求来,如果你已经有了稳定的 3.3V 电源了,就不再需要这一部分了。

以上就是这个模块的硬件电路分析,我们本实验使用的时候,直接 VCC、GND 接上电,SCL 和 SDA 接上 I2C 通信的 GPIO 口就行了,这就是硬件电路。

这就像是让你坐在车里,不看任何窗户,然后让你辨别当前车子的行驶方向,短时间内,你可以通过陀螺仪得知方向的变化,从而确定变化后的行驶方向。但是时间一长,车子到处转弯,你没有稳定的参考了,就肯定会迷失方向,所以这时候,你就要带个指南针在身边,提供长时间的稳定偏航角进行参考,来对陀螺仪感知的方向进行纠正,这就是 9 轴姿态传感器多出的磁力计的作用。另外如果你要制作无人机,需要定高飞行,这时候就还需要增加气压计,扩展为 10 轴,提供一个高度信息的稳定参考。

我们再看一下这个芯片的模块框图。这个图就是整个芯片的内部结构,其中左上角是时钟系统,有时钟输入脚和输出脚,不过我们一般使用内部时钟,硬件电路中 CLKIN 直接接了 GND,CLKOUT 没有引出,所以这部分不需要过多关心。 然后下面这些灰色的部分,就是芯片内部的传感器,其中包括 XYZ 轴的加速度计,XYZ 轴的陀螺仪,另外这个芯片还内置了一个温度传感器,你要是想用它来测量温度,也是没问题的。 那这么多传感器,本质上也都相当于可变电阻,通过分压后,输出模拟电压,然后通过 ADC,进行模数转换,转换完成之后,这些传感器的数据统一都放在数据寄存器中,我们读取数据寄存器就能得到传感器测量的值,这个芯片内部的转换,都是全自动进行的。 就类似我们之前学的 AD 连续转换 + DMA 转运,每个 ADC 输出,对应 16 位的数据寄存器,不存在数据覆盖的问题,我们配置好转换频率之后,每个数据就自动以我们设置的频率刷新到数据寄存器,我们需要数据的时候,直接来读就行了,其他的都不用管,还是非常方便的。 接着,每个传感器都有个自测单元,这部分是用来验证芯片好坏的,当启动自测后,芯片内部会模拟一个外力施加在传感器上,这个外力导致传感器数据会比平时大一些,那如何进行自测呢? 我们可以先使能自测,读取数据,再失能自测,读取数据,两个数据一相减,得到的数据叫自测响应,这个自测响应,芯片手册里给出了一个范围,如果自测响应在这个范围内,就说明芯片没问题,如果不在,就说明芯片可能坏了,使用的话就要小心点,这个是自测的功能。 然后下面是电荷泵,或者叫充电泵,CPOUT 引脚需要外接一个电容,什么样的电容呢,这个在手册里有说明。电荷泵是一种升压电路,在其他地方也有出现过,比如我们用的 OLED 屏幕,里面就有电荷泵进行升压。 电荷泵的升压原理(简单描述,了解一下即可):比如我有个电池,电压是 5V,然后再来个电容,首先电池和电容并联,电池给电容充电,充满之后,电容是不是也相当于一个 5V 的电池了。然后呢,关键部分来了,我再修改电路的接法,把电池和电容串联起来,电池 5V,电容也是 5V,这样输出就是 10V 的电压了,是不是凭空就把电池电压升高至两倍了啊。 不过由于这个电容电荷比较少,用一下就不行了,所以这个并联、串联的切换速度要快,趁电容还没放电完,就要及时并联充电,这样一直持续,并联充电,串联放电,并联充电,串联放电,然后后续再加一个电源滤波,就能进行平稳的升压了,这就是电荷泵的升压原理。 那这里,由于陀螺仪内部是需要一个高电压支持的,所以这里设计了一个电荷泵进行升压,当然这个升压过程是自动的,不需要我们管,了解一下即可。 然后右边这一大块,就是寄存器和通信接口部分了: 中断状态寄存器,可以控制内部的哪些事件到中断引脚的输出, FIFO,是先入先出寄存器,可以对数据流进行缓存,我们本节暂时不用。 配置寄存器,可以对内部的各个电路进行配置 传感器寄存器,也就是数据寄存器,存储了各个传感器的数据 工厂校准,这个意思就是内部的传感器都进行了校准,我们不用了解。 然后右边有个数字运动处理器,简称 DMP,是芯片内部自带的一个姿态解算的硬件算法,配合官方的 DMP 库,可以进行姿态解算,因为姿态解算还是比较难的,而且算法也很复杂,所以如果使用了内部的 DMP 进行姿态解算,姿态解算就会方便一些,当然我们本节学习 I2C 通信,这一块暂时不涉及。 FSYNC,是帧同步,我们用不到。 最后上面这块,就是通信接口部分了,上面一部分就是从机的 I2C 和 SPI 通信接口(8、9、23、24),用于和 STM32 通信;下面一部分是主机的 I2C 通信接口(7、8),用于和MPU6050 扩展的设备进行通信。 这里有个接口旁路选择器,就是一个开关,如果拨到上面,辅助的 I2C 引脚就和正常的 I2C 引脚接到一起,这样两路总线就合在一起了,STM32 可以控制所有设备,这时 STM32 就是大哥,MPU6050 和它的扩展设备都是 STM32 的小弟; 如果拨到下面,辅助的 I2C 引脚就由 MPU6050 控制,两条 I2C 总线独立分开,这时 STM32 是 MPU6050 的大哥,MPU6050 又是它的扩展设备的大哥,这样来连接的。如果使用的话,可以再详细研究。当然我们本节课程不会用到这个扩展功能。 然后最后,下面是供电的部分,按照手册里的电压要求和参考电路来接线就行了。

以上这些,就是这整个 MPU6050 芯片的结构了。

通过这些介绍,大家对 MPU6050 芯片和姿态解算的部分知识点就应该有个大致的了解了,接下来我们就一起来看一下这两个手册。还有一些额外的部分,再说一说。 首先是产品说明书,这个手册是 MPU6000 和 MPU6050 这两个芯片共用的,这两个型号的芯片功能都差不多,在细节上有一些区别,我们用的是 MPU6050 这个型号。

产品的整体介绍:比如说 3 轴 MEMS 陀螺仪、3 轴 MEMS 加速度计,这个 MEMS 就是这个公司研发的微机电系统,可以用电子的方式来进行姿态测量。 然后是,数字运动处理器 DMP,可以进行姿态解算,辅助的 I2C 通信可以扩展第三部分,比如磁力计。 下面,16 位的 ADC 进行数字化,可配置的满量程, 这些大家可以再详细看看,了解一下芯片的大体情况。

接下来有一个表给出了 MPU6000 和 MPU6050 的差异。总结下来就是两点:

MPU6050 有一个独立的逻辑电源引脚 VLOGIC,可以支持供电和 IO 口不一样的电平等级,而 MPU6000 没有MPU6000 同时支持 I2C 和 SPI 通信接口,而 MPU6050 仅支持 I2C。下面是通信引脚的差异 总共就是这两点差异,其他部分基本都是一样的。

然后下面是这个芯片的应用场景。比如图像稳定器、位置服务、游戏手柄、3D 遥控器、玩具等等。

接着下面这里是产品的特性列表,就是把芯片的各种功能、低功耗等等列举一下,大家可以详细看看。

然后下面是芯片的电气特性,就是各个性能的数据表格,如果要查询一些具体的数据指标,就可以在这个表里找找,一般芯片手册里,都会有这个电器特征的表格。

然后下面是 I2C 的时序特征,这个芯片的 I2C 的 SCL 时钟频率,最大可达 400 KHz。如果你的 SCL 时钟超过这个数值,那这个芯片可能就跟不上了。另外下面展示了各个电平,它的上升沿 下降沿以及持续时间,都要满足这个表里的要求。不过由于 I2C 是同步时序,对时间的要求不是非常严格,只要你的电平不是转瞬即逝的那么快,这个芯片都能跟得上,所以这个 400KHz 只是一个最大参数,实际的速率可以慢一些,这都没关系。

然后下面是 SPI 的时序要求,不过我们 MPU6050 这个芯片没有 SPI 的接口,

然后下面是绝对最大参数,超过这个参数范围,芯片就要损坏了,比如这个 VDD 供电范围是 -0.5V 到 +6V,超过 6V 就会损坏,不过之前电气特性表给的供电范围是 2.375~3.46V,所以最好还是不要直接加 5V 的电,超过电气特性的供电范围,芯片可能无法正常工作,超过绝对最大参数,芯片就要损坏了。这就是这两个参数的意义。

然后下面是应用信息,这里给出的是 MPU6000 和 MPU6050 这两个芯片的引脚定义,还有 XYZ 轴的定义图示,如果要理解每个引脚的具体解释,可以看一下上面的引脚描述,当然有很多引脚我们用不到,所以这个模块只引出了部分关键引脚。

接着下面是参考电路,哪个地方需要接个电容,电容的容值多大,这个图和下面的表,都有参考的示例,如果你要自己拿芯片画电路的话,就可以参考这里给出的示例。

然后再下面就是芯片的模块框图,刚详细介绍过。

下面是对每一部分的说明,可以再详细看看。

接着下面就是用辅助的 XCL 和 XDA 引脚把 6 轴传感器扩展成 9 轴传感器的电路了,辅助的 CL 和 DA,一般外挂一个磁力传感器,就可以扩展成 9 轴了,有一个 MPU6000 利用 SPI 接口扩展 9 轴的示例。

之后还是特性介绍。

然后下面是时钟系统,这个芯片的时钟,可以由一些时钟源提供

内部晶振,可以作为系统时钟。XYZ 轴的陀螺仪,它们也都会有个晶振,因为陀螺仪内部需要高精度时钟的支持,所以陀螺仪内部也有独立的时钟,这 3 个时钟也可以输出,作为系统时钟。然后是可以通过外部的 CLKIN 引脚,输入 32.768 KHz 的方波,或者 19.2 MHz 的方波,作为系统时钟。

不过这个外部时钟还需要额外的电路,比较麻烦,所以如果不是特别要求的话,我们一般用内部晶振或者内部陀螺仪的晶振,作为系统时钟,这个是时钟源的选择问题。

然后下面是可选的中断信号,可以配置,发生这些事件时,在 INT 引脚输出一个电平跳变,然后 STM32 可以用外部中断来接收这个跳变,这样中断信号就可以通往 STM32 的 CPU 了。

接下来介绍的是这个芯片内置的一些实用小功能,比如自由落体检测、运动检测和零运动检测,含有一张执行逻辑的图,简单来说就是,自由落体时,3 个加速度值均为 0,当加速度值低于预设的阈值时,开始计数,当计数足够多时,发生自由落体事件,可以配置这个事件去触发中断引脚的电平跳变,如果需要这功能的话,可以看一看这些文字说明。下面的运动检测和零运动检测也都是差不多的逻辑。另外注意就是,运动检测会有一个高通滤波器,用来滤除重力造成的稳定数据偏置,也就是滤除加速度计静置时,那 1 个 g 的重力加速度。就跟电容,隔直通交,差不多的功能,如果你要不用这个功能的话,也不用管这个高通滤波器。

之后就是数字接口,也就是 I2C 总线的介绍,这里就给出了这个设备的 I2C 地址 1101 000 或者 1101 001,

下面是 I2C 时序的介绍,我们上一小节也都讲过。

然后下面是数据帧格式,上面是指定地址写一个字节,开始,地址 + 写,应答,寄存器地址,应答,数据,应答,停止。下面是指定地址写多个字节,开始,地址 + 写,应答,寄存器地址,应答,数据,应答,数据,应答(两次读两个字节),停止。 然后是指定地址读一个字节,开始,地址 + 写,应答,寄存器地址,应答,重复开始,地址 + 读,应答,从机发数据,主机给非应答,停止。下面指定地址读多个字节,区别就是数据,应答,数据,非应答,在最后一个数据给非应答,然后停止。这些时序跟我们之前介绍 I2C 的时候是一样的。

然后下面是 SPI 接口。我们就不看了。

接着下面描述的是当你的供电电压和 IO 口的逻辑电压不一样时,应该如何接线和配置,这个看一下就行了,我们这里所有器件都是 3.3V,所以直接全部接 3.3V 即可,不存在这个问题。

那最后就是尺寸信息和 PCB 布线的参考了,这个画 PCB 的话,可以参考这些信息,这里也会提到一些注意事项和布线建议。

后面就是包装的一些信息了。

那到这里,我们这个产品说明书就看完了。然后我们继续看一下寄存器映像的手册。

其实寄存器还是非常重要的,看原理,你只能知道哪个是哪个,电路如何运作的,真正要写程序了,还是得研究寄存器才能明白,那我们 STM32 有库函数封装了,所以对寄存器要求不是很高,但是这个芯片的话,还是得需要手动配置一下寄存器,所以研究一下寄存器还是很有必要的。

这个手册的前面有个寄存器的总表,这个表里寄存器非常多,但是我们不需要全都了解,简单应用的话,了解其中一部分就行了。

第一列,是寄存器的地址,16 进制表示的;第二列,是寄存器地址,10 进制表示的;第三列是寄存器的名称;第四列是读写权限,RW 代表可读可写,R 代表只读,之后后面是寄存器内的每一位的名字。每个寄存器的都是 8 位的.

那在这里,我们需要了解并一一说明的寄存器依次是:

采样频率分频器

里面的 8 位为一个整体,作为分频值,这个寄存器可以配置采样频率的分频系数。简单来说就是,分频越小,内部的 AD 转换就越快,数据寄存器的刷新就越快,反之就越慢。这里有个公式,采样频率(你可以理解为数据刷新率) = 陀螺仪输出时钟频率 /(1 + 分频值)。这个时钟,就是我们刚才说的那几个时钟源(内部晶振,陀螺仪晶振和外部时钟引脚的方波),这里直接是以陀螺仪晶振作为例子,陀螺仪时钟 / 这个寄存器指定的分频系数,就是采样频率。

注意事项:不使用低通滤波器时,陀螺仪时钟为 8KHz,使用滤波器时,时钟就是 1KHz 了,这个了解一下就行了。

配置寄存器

内部有两部分,外部同步设置和低通滤波器配置。

这个外部同步,我们不用,可以不看。然后是低通滤波器,配置这些位可以选择手册这里的各种滤波参数(0~7),这个低通滤波器可以让输入数据更加平滑,配置滤波器参数越大,输出数据抖动就越小,0 是不使用低通滤波器,陀螺仪时钟为 8KHz,之后使用了滤波器,陀螺仪时钟就是 1KHz,这个刚才说过,然后这个最大的参数,是保留位,没有用到。

这就是配置寄存器,可以配置低通滤波器,让数据更平滑。

陀螺仪配置寄存器

高 3 位是 XYZ 轴的自测使能位,中间两位是满量程选择位,后面 3 位没用到。

对于自测的用法,手册这里有描述,之前也介绍过,手册这里有个公式:自测响应 = 自测使能时的数据 - 自测失能时的数据。我们上电后,先使能自测,读取数据,再失能自测,读取数据,两者相减,得到自测响应,然后在产品说明书手册的电气特性表里找一下自测响应的范围,如果在这个范围里,芯片就通过了自测,之后正常使用即可。这是自测。

然后是满量程选择,可以选择四种(0~3),这个之前也介绍过,量程越大,范围越广;量程越小,分辨率越高。

加速度计配置寄存器

和上面基本一样,高 3 位是自测使能位,中间两位是满量程选择位,不过后面 3 位是配置高通滤波器的。这个高通滤波器,刚才说过,就是内置小功能,运动检测用到的,对数据输出没有影响,我们暂时用不到。下面的描述和刚才的基本一致。

数据寄存器,包括加速度计 XYZ 轴,温度传感器、陀螺仪 XYZ 轴的数据,这里 _L 表示低 8 位,_H 表示高 8 位。

首先是加速度计的数据寄存器,我们想读取数据的话,直接读取数据寄存器就行了,这是一个 16 位的有符号数,以二进制补码的方式存储,我们读出高 8 位和低 8 位,高位左移 8 次,或上低位数据,最后再存在一个 int16_t 的变量里,这样就可以得到数据了。

下面温度传感器数据、陀螺仪数据,都是一样的操作方法。

电源管理寄存器 1 和 2

电源管理寄存器 1 : 第一位,设备复位,这一位写 1,所有寄存器都恢复到默认值。 第二位,睡眠模式,这一位写 1,芯片睡眠,芯片不工作,进入低功耗。 第三位,循环模式,这一位写 1,设备进入低功耗,过一段时间,启动一次,并且,唤醒的频率,由下一个寄存器的前两位决定,这个模式比较省电。 第五位,温度传感器失能,写 1 之后,禁用内部的温度传感器, 最后三位,用来选择系统时钟来源,手册下面有个表,分别是:内部晶振(0),XYZ轴陀螺仪晶振(1~3),外部引脚的两个方波(4、5),和我们之前介绍的时钟源一样,一般我们选择内部晶振或者陀螺仪晶振,不过手册里非常建议我们选择陀螺仪晶振,因为陀螺仪的晶振更加精确,这就是这个寄存器。

电源管理寄存器 2 : 前两位刚才说过。 后面 6 位,可以分别控制 6 个轴进入待机模式,如果你只需要部分轴的数据,可以让其他轴待机,这样比较省电。

器件 ID 号(WHO_AM_I)

这个寄存器是只读的,ID 号不可修改,ID 号中间这 6 位,固定为 110100,实际上这个 ID 号,就是这个芯片的 I2C 地址,它的最高位和最低位,其实都是 0,那读出这个寄存器,值就固定为 0x68。然后这里有句话:AD0 引脚的值,并不反映在这个寄存器上,意思就是,之前我们说过,这个 I2C 地址,可以通过 AD0 引脚进行配置,但是,这里的 ID 号的最低位,是不随 AD0 引脚变化而变化的,读出 ID 号,始终都是 0x68。当前这个 ID 号也不是非要和 I2C 地址一样,这是它这样设计的而已。

其他的暂时可以不看。

最后手册里有句话:所有的寄存器上电默认值都是 0x00,除了 107 号寄存器,上电默认 0x40,117 号寄存器,上电默认 0x68。 117 号,就是 ID 号,默认 0x68,这个没问题。 107 号,是电源管理寄存器 1,默认 0x40,也就是次高位为 1,这里次高位是 SLEEP,所以这个芯片上电默认就是睡眠模式,我们在操作它之前,要先记得解除睡眠。否则操作其他寄存器是无效的。这个注意一下。

好那到这里,我们这个寄存器就看完了。好那本小节,有关 MPU6050 的内容,到这里就差不多了。我们接下来的任务,就是实现 I2C 通信,然后读写这里的寄存器,来操控 MPU6050。

6.2 I2C 外设简介

通过 I2C 通信章节,我们已经了解了 I2C 的协议规定和通信意义,并且我们也用 GPIO 口模拟的 I2C,实现了读写 MPU6050 的程序。在这个过程中,我们可以发现,通信协议的时序是一个很重要的东西,我们只要理解清楚了时序的意义,就可以按照它协议的规定,去翻转通信引脚的高低电平,只要我们翻转产生的这个时序波形满足了通信协议的规定,那通信双方就能理解并解析这个波形,这样,通信自然而然就实现了。

那之前我们用的是软件 I2C 手动拉底或释放时钟线,然后再手动对每个数据位进行判断,拉底或释放数据线,这样来产生这个波形,这是软件 I2C。由于 I2C 是同步时序,这每一位的持续时间要求不严格,某一位的时间长点短点,或者中途暂停一会儿时序,影响都不大,所以 I2C 是比较容易用软件模拟的,在实际项目中,软件模拟 I2C 也是非常常见的。

但是作为一个协议标准,I2C 通信,也是可以有硬件收发电路的,就像之前的串口通信一样,我们先讲了这个串口的时序波形,但是在程序中呢,我们并没有软件去手动翻转电平来实现这个波形,这是因为串口是异步时序,每一位的时间要求很严格,不能过长也不能过短,更不能中途暂停不会。所以串口时序虽然可以用软件模拟但是操作起来比较困难,我们之前也没介绍过软件模拟的串口,所以就没有手动翻转电平这个操作;另外,由于串口的硬件收发器在单片机中普及程度非常高,基本上每个单片机都有串口的硬件资源,而且硬件实现的串口使用起来还非常简单,所以串口通信,我们基本都是借助硬件收发器来实现的。

硬件串口的使用流程,就是之前写程序演示的,首先配置 USART 外设,然后写入数据寄存器 DR,这时硬件收发器就会自动生成波形发送出去,最后我们等待发送完成的标志位即可,这就是硬件实现串口的方法。

那回到 I2C 这里来,I2C 也可以有软件模拟和硬件收发器自动操作这两种实现方式。对于串口这样的异步时序呢,软件实现,非常麻烦;硬件实现,非常简单,所以串口的实现基本是全都倒向硬件了;而对于 I2C 这样的同步时序来说,软件实现反而简单且灵活,硬件实现相比之下却不能完全让人省心,所以 I2C 的实现,软件模拟的情况还是非常多的,但是考虑到硬件 I2C 也有很多独有的优势,比如执行效率比较高,可以节省硬件资源,功能比较强大,可以实现完整的多主机通信模型,时序波形规整,通信速率快,等等。所以硬件 I2C 也是有相应的应用场景的,如果你只是简单应用,可以选择比较灵活的软件 I2C,如果你对性能指标要求比较高,就可以考虑一下硬件 I2C。好,到这里,软件实现协议和硬件实现协议,这两种方法的操作流程,大家就应该了解了。

那进入本小节的整体,我们来介绍 I2C 的硬件实现,也就是 STM32 内部的 I2C 外设,这一部分硬件 I2C 的程序现象和之前软件 I2C 的现象一样。我们直接来看 I2C 外设的简介。

TM32内部集成了硬件 I2C 收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担

就像之前 USART 外设,它是串口通信的硬件收发器,这里 I2C 的外设,就是 I2C 通信的硬件收发器。有了硬件收发器之后,就可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,也就是由硬件电路来自动翻转引脚电平,软件呢,只需要写入控制寄存器 CR 和数据寄存器 DR 就可以实现协议了。当然为了实时监控时序的状态,软件还得读取状态寄存器 SR,来了解外设电路当前处于什么状态。这就像是开车一样,写入控制寄存器 CR,就像是踩油门、打方向盘,来控制汽车的运行;读取状态寄存器 SR,就像是观看仪表盘,来了解汽车的运行状态,有了这些寄存器,我们就可以完全掌控外设电路的运行了。 那我们 STM32 有库函数封装之后,这个操作就更简单了,调用函数,给个参数,库函数就能自动配置或读取各种寄存器了。 然后继续,有了这个 I2C 外设的存在,硬件自动实现时序,就可以减轻 CPU 的负担,节省软件资源。另外由硬件来做这个事情,可以更加专注,时序生成的性能、效率也会更高,这就是 I2C 外设存在的意义。就是用硬件电路,实现 I2C 通信。

然后接着看下面,就是硬件 I2C 的功能介绍了

支持多主机模型 支持 7 位/ 10 位地址模式 支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz) 支持 DMA 兼容 SMBus 协议

这是硬件 I2C 的功能指标,这些内容大家都只做了解即可,不需要完全掌握。因为 I2C 协议还是比较庞大的,这个硬件 I2C 呢,作为一个专业户,它的设计指标非常高,基本支持 I2C 协议的所有规定,但是我们在实际应用中,往往只需要那个最简单、最常用的应用领域,也就是普通的一主多从模型、7 位地址模式。所以,如果你完成没学过 I2C 通信,想自己看这个参考手册去学习 I2C,那基本是学不会的。因为这个手册上来就是 I2C 的完全体,使用多主机的模型,这里包括从发送、从接收、主发送、主接收 4 种模式,然后后面就一会是从模式,一会是主模式,一会几个模式互相切换,还有 7 位地址和 10 位地址,等等。总之,设计的比较复杂,这个手册,只有你先把最简单的 I2C 通信学会了,再来进阶的看,这才容易理解,所以我们本节的内容,只要求掌握一主多从、7 位地址的 I2C;对于多主机模型、10 位地址这些东西,全都只需要了解即可,但是为了方便大家理解后续的内容,我这里也都大概的介绍一下。

首先多主机模型,这个之前也介绍过,I2C 通信,分为主机和从机。顾名思义,主机,就是拥有主动控制总线的权力,而从机,只能在主机允许的情况下,才能控制总线。在一主多从的模型下,假设 I2C 总线上面是唯一的主机,下面可以挂载多个从机。那这个过程就很容易操作了,主机一个人掌控所有,所有从机都得听它的话,不存在什么权力冲突,这是一主多从。那进阶版的 I2C,还设计了多主机的模型,对于多主机模型,又可以分为固定多主机和可变多主机,固定多主机就是总线上有两个或更多个固定的主机,上面这几个,始终固定为主机;下面这几个,始终固定为从机,这个状态,就像是在教室里,讲台上同时站了多个老师,下面坐的所有学生,可以被任意一个老师点名,老师可以主动发起对学生的控制,学生不能去控制老师。当两个老师同时想说话时,就是总线冲突状态,这时就要进行总线仲裁了,总裁失败的一方让出总线控制权,那目前这种,讲台上站多个老师的情况,就是固定多主机。然后是可变多主机,这个模型的意思是,假设 I2C 总线可以挂载多个设备,总线上没有固定的主机和从机,任何一个设备,都可以在总线空闲时跳出来作为主机,然后指定其他任何一个设备进行通信,当这个通信完成之后,这个跳出来的主机就会退回到从机的位置,这就像是在教室里,只有一堆学生,没有老师,默认情况下,所有学生都是从机,都不能说话,当有某个学生想说话时,就得跳出来,变成主机,然后指定其他任何一个学生进行通信,通信完成后,再坐下,变成从机,当有多个学生同时跳出来时,就是总线冲突状态,这时就要进行总线仲裁,仲裁失败的一方让出总线控制权,那这种所有设备一视同仁,谁要做主机,谁就跳出来的模型,就是可变多主机。对于我们 STM32 的 I2C 而言,它使用的是可变多主机的模型,虽然我们只需要一主多从,没人跟 STM32 去争主机的位置,但是 STM32 是按照可变多主机的模型设计的,所以我们还是得按照,谁要做主机,谁就跳出来的思路来操作,这就是 STM32 I2C 的多主机设计,了解一下。接下啦继续看 7位 / 10位地址模式,从我们之前的 I2C 时序可以看到,我们这里使用的是 7 位地址的模式,也就是起始条件之后,紧跟的一个字节必须是 7 位地址 + 读写位,这种 7 位地址是最简单最常见的,那很显然,7 位地址只有 128 种情况,如果设备非常多,那就不够用了,如果一条总线必须要挂载 128 个以上的设备,那 7 位地址必然是不够用的,另外,如果有非常多的厂商都来申请 I2C 的地址,那也必然会有部分型号的芯片,它们的地址是一样的,对于不同芯片地址一样,其实也好办,因为地址的低位通常是可配置的,前面都一样,我们配置后面不一样就行了,只要你不在一个总线上挂载过多的设备就行,另外即使确实需要很多的设备,条件允许的情况下,也可以开辟多条 I2C 总线,所以地址的问题,一般好解决。当然,在协议上,分配更多的地址,也是一种可行的方案,那 STM32 的 I2C 总线就支持 10 位地址模式,10 位地址,最多就有 1024 种可能了,那 10 位地址是如何设计的呢?之前我们说了,I2C 起始之后的第一个字节,必须是寻址 + 读写位,这一个字节只能有 7 位地址,那我只需要再规定,起始之后的前两个字节都作为寻址,这不就行了嘛,这就是 10 位地址的基本思路。那第一个字节有 7 个空位,第二个字节,有 8 个空位,按理说加一起是 15 位地址,但我们 I2C 只有 10 位地址模式,还有 5 位跑哪去了呢?答案是,这 5 位当标志位去了,因为你发送第一个字节之后,谁知道你后面这个字节还是不是寻址呢,所以这就需要在第一个字节写个特定的数据,作为 10 位寻址模式的标志位,这个标志位就是 11110,也就是如果你第二个字节也是寻址,那第一个字节的前 5 位就必须是 11110。例如前 5 位是 11010,就说明它是 7 位寻址的,如果前 5 位是 11110,那么这第一个字节剩下的 2 位和第二个字节的 8 位,就都作为寻址,这不就是 10 位地址了嘛。当然 11110 开头的地址,作为 10 位地址模式的标志位,它是不会在 7 位地址模式下出现的,这就是 7 位地址和 10 位地址的区别,大家了解一下即可。然后继续看支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)。这个速度,是协议规定的标准速度,也就是说,如果某个设备声称支持快速的 I2C,那它就支持最大 400 kHz 的时钟频率。当然,作为一个同步协议,这个时钟并不严格,所以你只要不超过这个最大频率,多少都可以,所以这个频率的具体值,我们一般关注不多。接着下一条,支持 DMA,这个在多字节传输的时候可以提高传输效率,比如指定地址读多字节或写多字节的时序,如果我想要连续读或写非常多的字节,那用一下 DMA 自动帮我们转运数据,这个过程的效率就会大大提升。当然如果你只有几个字节,那就没必要用 DMA 了,这个是 DMA 配合通信的应用。然后继续,是兼容 SMBus 协议,这个 SMBus(System Management Bus),是系统管理总线,SMBus 是基于 I2C 总线改进而来的,主要用于电源管理系统中,SMBus 和 I2C 非常像,所以 STM32 的 I2C 外设就顺便兼容了一下 SMBus,这个了解一下即可。我们主要学习的是 I2C。

最后看一下我们这个型号芯片 STM32F103C8T6 硬件I2C资源:I2C1、I2C2 两个独立的 I2C 外设。

可以看出,这里资源的限制也是硬件 I2C 和 软件 I2C 的区别之一,因为硬件 I2C 必须要有硬件电路的支持,所以硬件 I2C 的资源是有限的,比如我们这个型号的 STM32,最多就只能有两路硬件 I2C 的总线,但是对于软件 I2C,资源一般没有很大的限制,我们只需要复制一下代码,就可以开辟一路新的 I2C 总线,所以软件 I2C,只要代码能存的下,基本上是想开几路就开几路,没有资源的限制。这是硬件 I2C 和软件 I2C 资源的差异。好,那这些就是 STM32 I2C 外设的简介。

那了解清楚了 I2C 外设的用途和设计理念,我们就来看一下这个 I2C 外设的框图。

这里展示的就是 STM32 内部 I2C 外设的结构图。我们看一下

首先,左边是这个外设的通信引脚 SDA 和 SCL,这就是 I2C 通信的两个引脚,这个 SMBALERT 是 SMBus 用的,I2C 用不到,不用管的,那像这种外设模块引出来的引脚,一般都是借助 GPIO 口的复用模式与外部世界相连的,具体是复用在了哪个 GPIO 口呢,还是查询引脚定义表,在复用功能这两栏里找一下,比如 I2C2 的 SCL 和 SDA 就复用在了 PB10 和 PB11,这两个端口;然后,I2C1 的 SCL 和 SDA 复用在了 PB6 和 PB7 两个引脚,另外,这里 I2C1 的两个引脚还有重映射的机会,可以重映射到 PB8 和 PB9 两个引脚,这就是硬件 I2C 外设与 GPIO 口的复用关系。那引脚看完,我们继续看内部电路

因为内部电路设计的时候,这些引脚就是连接好了的,所以如果想使用硬件 I2C,就只能使用它连接好的指定引脚,不像软件 I2C 那样,引脚可以任意指定。硬件 I2C,引脚就是固定的这几个,不能任意更改,所以硬件 I2C,对引脚的限制也比较大,这个注意一下。

首先上面这一块,是 SDA,也就是数据控制部分,这里数据收发的核心部分,是这里的数据寄存器和数据移位寄存器,当我们需要发送数据时,可以把一个字节数据写到数据寄存器 DR;当移位寄存器没有数据移位时,这个数据寄存器的值就会进一步转到移位寄存器里,在移位的过程中,我们就可以直接把下一个数据放到数据寄存器里等着了,一旦前一个数据移位完成,下一个数据就可以无缝衔接,继续发送,当数据由数据寄存器转到移位寄存器时,就会置状态寄存器的 TXE 位为 1,表示发送寄存器为空,这就是发送的流程。那在接收时,也是这一路,输入的数据,一位一位地从引脚移入到移位寄存器里,当一个字节的数据收齐之后,数据就整体从移位寄存器转到数据寄存器,同时置标志位 RXNE,表示接收寄存器非空,这时我们就可以把数据从数据寄存器读出来了。这个流程和之前串口那里是不是一样,串口数据收发也是由数据寄存器和移位寄存器两级实现的,只不过串口是全双工,这里数据收和发是分开的,在 I2C 这里,是半双工,所以数据收发,是同一组数据寄存器和移位寄存器,但是这个数据寄存器和移位寄存器的配合,设计思路都是异曲同工。那有了这一块,SDA 的数据收发就可以完成了,至于什么时候收,什么时候发,需要我们写入控制寄存器的对应位进行操作,对于起始条件、终止条件、应答位什么的,这里也都有控制电路可以完成,至于具体实现细节,这里也没详细画,大家知道有电路可以完成这些工作就行了。 那数据收发之后,下面这里还有两个功能,一个是比较器和自身地址寄存器、双地址寄存器;另一个是帧错误校验计算和帧错误校验寄存器。首先说一下,这两块内容,一般用不到,了解一下即可。那这里,比较器和地址寄存器这是从机模式使用的,刚才说了 STM32 的 I2C 是基于可变多主机模型设计的,STM32 不进行通信的时候,就是从机,既然作为从机,它就应该可以被别人召唤,想被别人召唤,它就应该有从机地址吧,从机地址是多少呢?就可以由这个自身地址寄存器指定。我们可以自定一个从机地址,写到这个寄存器,当 STM32 作为从机,在被寻址时,如果收到的寻址通过比较器判断,和自身地址相同,那 STM32 就作为从机,响应外部主机的召唤,并且这个 STM32 支持同时响应两个从机地址,所以就有自身地址寄存器和双地址寄存器,这一块,我们需要在多主机的模型下来理解,把角色转换一下,STM32 作为从机才需要有这一部分,当然我们只要求一主多从的模型,STM32 不会作为从机,所以这一块就不需要用。 然后右边这一块也是进阶的内容,这是 STM32 设计的一个数据校验模块,当我们发送一个多字节的数据帧时,在这里硬件可以自动执行 CRC 校验计算,CRC 是一种很常见的数据校验算法,它会根据前面这些数据,进行各种数据运算,然后会得到一个字节的校验位附加在这个数据帧后面,在接收到这一帧数据后,STM32 的硬件也可以自动执行校验的判定,如果数据在传输过程中出错了,CRC 校验算法就通不过,硬件就会置校验错误标志位,告诉你数据错了,使用的时候注意点,这个校验过程就跟串口的奇偶校验差不多,也是用于进行数据有效性验证的,当然这一块我们也不会用到,所以也是了解即可。那 SDA 这一块,我们就只需要掌握这个数据寄存器和移位寄存器配合的这部分就行了。然后继续看下面 SCL 的这部分,其实看着也没啥东西,时钟控制,是用来控制 SCL 线的,至于控制的细节,这里也没画,你就把它当作是一个黑盒子就行了,在这个时钟控制寄存器写对应的位,电路就会执行对应的功能;然后控制逻辑电路,也是黑盒子,写入控制寄存器,可以对整个电路进行控制;读取状态寄存器,可以得知电路的工作状态;之后是中断,当内部有一些标志位置 1 之后,可能事情比较紧急,就可以申请中断,如果我们开启了这个中断,那当这个事件发生后,程序就可以跳到中断函数来处理这个事件了;最后是 DMA 请求与响应,在进行很多字节的收发时,可以配合 DMA 来提高效率,这个也了解一下。

那这些就是这个 I2C 外设的框图。其实也没有很多东西,大部分都是黑盒模型。

接着我们看一下基本结构图 这个结构图画的也是比较简单,我们把 I2C 框图中用不到的东西去掉,然后整理一下,得到内部的简化结构就是 I2C 基本结构图。

首先移位寄存器和数据寄存器 DR 的配合是通信的核心部分,这里因为 I2C 是高位先行,所以移位寄存器是向左移位,在发送的时候,最高位先移出去,然后是次高位,等等。一个 SCL 时钟,移位一次,移位 8 次,这样就能把一个字节,由高位到低位,依次放到 SDA 线上了,那在接收的时候呢,数据通过 GPIO 口从右边依次移进来,最终移 8 次,一个字节就接收完成了。之后 GPIO 口这里,使用硬件 I2C 的时候,这两个对应的 GPIO 口,都要配置成复用开漏输出的模式,复用,就是 GPIO 的状态是交由片上外设来控制的,开漏输出,这是 I2C 协议要求的端口配置。之前也说过,这里即使是开漏输出模式,GPIO 口也是可以进行输入的。然后 SCL 这里,时钟控制器通过 GPIO 去控制时钟线,这里简化成一主多从的模型了,所以时钟这里只画了输出的方向,实际上 I2C 框图这里,如果是多主机的模型,时钟线也是会进行输入的,这个时钟的输入,可以先不管。然后继续,SDA 的部分,输出数据,通过 GPIO,输出到端口;输入数据,也是通过 GPIO,输入到移位寄存器,那这两个箭头连接在 GPIO 的哪个位置呢?在复用开漏/推挽输出模式,我们要使用开漏输出,所以 P-MOS 是没有的,然后移位寄存器输出的数据,通向 GPIO,就接在了来自片上外设的复用功能输出,所以,I2C 外设的输出,就接到这里,之后控制这个 N-MOS 的通断,进而控制这个 I/O 引脚,是拉底到低电平,还是释放悬空。然后对于输入部分,虽然是复用开漏输出,但是输入这一路仍然有效,I/O 引脚的高低电平,通过进入片上外设,来进行复用功能输入,所以 I2C 外设通向 GPIO,输出就接在了复用功能输出,输入就接在了复用功能输入。回到这个结构图,移位寄存器到 GPIO 是 GPIO 复用输出,GPIO 到移位寄存器是 GPIO 复用输入,这就是 I/O 引脚,这是这一部分。然后数据控制器,这是黑盒模型,没啥说的。最后还是有个开关控制,也就是 I2C_Cmd,配置好了就使能外设,外设就能正常工作了。

这些就是这个 I2C 外设的基本结构图。

接下来,我们来看一下硬件 I2C 的操作流程。这两张图展示的是主机发送和主机接收的操作流程,这个操作流程图就告诉了我们,要想产生这样的 I2C 时序,啥时候该干些啥,啥时候会产生啥事件,我们写程序的时候,也就是参考这个流程来写的,所以还是要仔细分析一下,那在手册里,是给出了从机发送、从机接收、主机发送、主机接收四个流程图。当然从机部分我们暂时不管,所以这里只看主机发送和主机接收的流程即可。

主机发送 我们先看一下主机发送,当 STM32 想要执行指定地址写的时候,就要按照主发送器传送序列图来进行,这里有 7 位地址的主发送和 10 位地址的主发送。它们的区别就是 7 位地址,起始条件后的一个字节是寻址,10 位地址,起始条件后的两个字节都是寻址,其中前一个字节,这里写的是帧头,内容是 5 位的标志位 11110 + 2 位地址 + 1 位读写位,然后后一个字节,内容就是纯粹的 8 位地址了,两个字节加一起,构成 10 位的寻址,这是 10 位地址的寻址方式,了解一下。当然我们主要关注 7 位地址的就行了,7 位主发送这个时序呢,流程是起始,从机地址,应答,后面是数据 1,应答,数据 2,应答等等,这样来表示的,最后是 P,停止。因为 I2C 协议只规定了起始之后必须是寻址,至于后面数据的用途并没有明确的规定,这些数据可以由各个芯片厂商自己来规定。比如 MPU6050 规定就是:寻址之后,数据 1 为指定寄存器地址,数据 2 为指定寄存器地址下的数据,之后的数据 N,就是从指定寄存器地址开始,依次往后写,这就是一个典型的指定地址写的时序流程。

然后我们从头来看一下,首先,初始化之后,总线默认空闲状态,STM32 默认是从模式,为了产生一个起始条件,STM32 需要写入控制寄存器,这个得看一下手册的寄存器描述。在控制寄存器 CR1 中,有个 START 位,在这一位写 1,就可以产生起始条件了,当起始条件发出后,这一位可以由硬件清除,所以,只要在这一位写 1,STM32 就自动产生起始条件了,这就是我们刚才说的,写入控制寄存器,就像是踩油门刹车,来控制硬件运行。之后,STM32 由从模式转为主模式,也就是多主机模型下,STM32 有数据要发,就要跳出来,这个意思。

然后,控制完硬件电路之后,我们就要检查标志位,来看看硬件有没有达到我们想要的状态,在这里,起始条件之后,会发生 EV5 事件,这个 EV5 事件,你就可以把它当成是标志位,手册里都是用 EV(Event)几 这个事件来代替标志位的,为什么要设计这个 EV几,EV几 事件,而不直接说产生什么标志位呢,这是因为,有的状态会同时产生多个标志位,所以这个 EV几 事件就是组合了多个标志位地一个大标志位,在库函数中也有对应的,检查 EV几 事件是否发生的函数,所以就当成是一个大标志位来理解就行了。下面解释了 EV5 事件就是 SB(Start Bit)标志位为 1,SB 是状态寄存器的一个位,表示了硬件的状态,就像是开车时,看一下仪表盘这个意思。在手册的状态寄存器 SR1 中可以找到这一位,这一位置 1,代表起始条件已发送,软件读取 SR1 寄存器后,也就是查看了这一位,然后写数据寄存器的操作,将清除该位,写数据寄存器 DR,就是我们接下来的操作,所以按照正常的流程来,这个状态寄存器是不需要手动清除的。

然后继续这个流程,当我们检测起始条件已发送时,就可以发送一个字节的从机地址了,从机地址需要写到数据寄存器 DR 中,写入 DR 之后,硬件电路就会自动把这一个字节,转到移位寄存器里,再把这一个字节发送到 I2C 总线上,之后硬件会自动接收应答位并判断,如果没有应答,硬件就会置应答失败的标志位,然后这个标志位可以申请中断来提醒我们,在寻址完成之后,会发生 EV6 事件,下面解释了 EV6 事件,就是 ADDR 标志位为 1,在手册中可以找到,ADDR 标志位在主模式状态下就代表地址发送结束。

然后继续看,EV6 事件结束后,是 EV8_1 事件,下面解释,EV8_1 事件就是 TxE 标志位 = 1,移位寄存器空,数据寄存器空,这时需要我们写入数据寄存器 DR 进行数据发送了,一旦写入 DR 之后,因为移位寄存器也是空,所以 DR 会立刻转到移位寄存器进行发送,这时就是 EV8 事件,移位寄存器非空,数据寄存器空,这时就是 移位寄存器正在发数据的状态,所以流程这里,数据 1 的时序就产生了,这个数据寄存器和移位寄存器的配合,要把 I2C 基本结构记好,就是发送的时候,数据先写入数据寄存器,如果移位寄存器没有数据,再转到移位寄存器进行发送,这个流程要理解清楚。否则解释这里写的移位寄存器非空,数据寄存器空,你可能就不知道是什么意思了。

那继续看,当 EV8 事件没有时,对应下面解释,写入 DR 将清除该事件,所以按理说,此时应该是写入了下一个数据,也就是后面这个数据 2,在这个时刻就被写入到数据寄存器里等着了,然后接收应答位之后,数据 2 就转入移位寄存器进行发送,此时的状态是移位寄存器非空,数据寄存器空,所以这时,这个 EV8 事件就又发生了。之后 EV8 事件没有时,数据 2 还正在移位发送,但此时下一个数据,已经被写到数据寄存器等着了,所以这个时候 EV8 事件消失,之后应答,产生 EV8 事件,写入数据寄存器,EV8 事件消失。按照这个流程来,一旦我们检测到 EV8 事件,就可以写入下一个数据了。

最后,当我们想要发送的数据写完之后,这时就没有新的数据可以写入到数据寄存器了。当移位寄存器当前的数据移位完成时,此时就是移位寄存器空,数据寄存器也空的状态,这个事件就是这里的 EV8_2,下面解释,EV8_2 是 TxE = 1,也就是数据寄存器空,BTF(Byte Transfer Finished),这个是字节发送结束标志位,手册里可以看一下解释:在发送时,当一个新数据将被发送且数据寄存器还未被写入新的数据时,BTF 标志位置 1,这个意思就是,当前的移位寄存器已经移完了,该找数据寄存器要下一个数据了,但是一看,数据寄存器没有数据,这就说明主机不想发了,这时就代表字节发送结束,是时候停止了。所以当检测到 EV8_2 时,就可以产生终止条件了,产生终止条件,显然,应该在控制寄存器里有相应的位可以控制,手册里,控制寄存器 CR1 中有一位 STOP,写 1,就会在当前字节传输,或在当前起始条件发出后产生停止条件。

那到这里,一个完整的时序就发送完成了。这整个过程看上去可能比较复杂,操作和事件都比较多。但是简单来说就是,写入控制寄存器 CR 或者数据寄存器 DR 就可以控制时序单元的发生。比如产生起始条件,发送一个字节数据,时序单元发生后,检查相应的 EV 事件,其实就是检查状态寄存器 SR,来等待时序单元发送完成,然后依次按照这个流程,操作,等待,操作,等待,等等等等,这样就能实现时序了。当然在程序中,我们有库函数,不需要实际去配置寄存器的,所以这个过程会比想象中简单一些,等我们写程序的时候再来进一步理解。现在这个操作流程,大家先有个印象就行。

这就是主机发送的流程,接着我们继续看主机接收的流程。

主机接收 这里有 7 位主接收和 10 位主接收,从这个 7 位主接收的时序看,这里时序的流程是:起始,从机地址+读,接收应答,然后就是,接收数据,发送应答,接收数据,发送应答,最后一个数据,给非应答,之后终止。可以看出,这个时序应该是当前地址读的时序,指定地址读的复合格式,这里没有给,得需要我们自己组合一下,然后下面 10 位地址的当前地址读就复杂一些了。这里是,起始,发送帧头,这个帧头里的读写位,应该还是写的,因为后面还要跟着发送第二个字节的地址,之后继续发送第二个字节的 8 位地址,这样才能进行寻址,然后要想转入读的时序,必须再发送重复起始条件,发送帧头,这次帧头的读写位就是读的了,因为发送读的指令之后,必须要立刻转入读的时序,所以这第二个字节的地址就没有了,直接转入接收数据的时序,这是 10 位地址的操作流程,稍微复杂一些,当然我们主要还是看 7 位地址的就行。

首先写入控制寄存器的 START 位,产生起始条件,然后等待 EV5 事件,下面解释和刚才一样,EV5 事件就代表起始条件已发送,之后是寻址,接收应答,结束后产生 EV6 事件,下面的解释也和刚才一样,EV6 事件代表,寻址已完成,之后数据 1 这一块,代表数据正在通过移位寄存器进行输入,EV6_1 事件,下面解释是没有对应的事件标志,只适于接收 1 个字节的情况,这个 EV6_1,可以看到,数据 1 其实还正在移位,还没收到呢,所以这个事件就没有标志位。之后当这个时序单元完成时,硬件会自动根据我们的配置,把应答位发送出去,如何配置是否要给应答呢?也是看手册,控制寄存器 CR1 里,这里有一位 ACK,应答使能,如果写 1,在接收到一个字节后就返回一个应答,写 0,就是不给应答,这就是应答位的配置。

之后继续,当这个时序单元结束后,就说明移位寄存器就已经成功移入一个字节的数据 1 了,这时,移入的一个字节就整体转移到数据寄存器,同时置 RxNE 标志位,表示数据寄存器非空,也就是收到了一个字节的数据,这个状态就是 EV7 事件。下面解释是,RxNE = 1,数据寄存器非空,读 DR 寄存器清除该事件,也就是,收到数据了,当我们把数据读走之后,这个事件就没有了,上面这里,EV7 事件没有了,说明此时数据 1 被读走了,当然数据 1 还没读走的时候,数据 2 就可以字节移入移位寄存器了,之后数据 2 移位完成,收到数据 2,产生 EV7 事件,读走数据 2,EV7 事件没有了。

然后按照这个流程,就可以一直接收数据了,最后,当我们不需要继续接收时,需要在最后一个时序单元发生时,提前把刚才说的应答位控制寄存器 ACK 置 0,并且设置终止条件请求,这就是 EV7_1 事件,下面解释,和 EV7 一样,后面加了一句,设置 ACK = 0 和 STOP 请求,也就是我们想要结束了。之后,在这个时序完成后,由于设置了 ACK = 0,所以这里就会给出非应答,最后由于设置 STOP 位,所以产生终止条件。

这样接收一个字节的时序就完成了。整体上,流程和刚才差不多,写入控制寄存器 CR 和读取数据寄存器 DR 产生时序单元,然后等待相应的事件,来确保时序单元的完成,这就是主机接收的时序流程。

好,这些就是主机发送和主机接收的操作流程,等我们写程序的时候,就对应这两个流程图,来实现硬件 I2C 的代码。

那接下来,我们再看一下软件 I2C 和硬件 I2C 的波形对比,这个图,大家了解即可,可以加深你对 I2C 协议的理解。这里是用示波器抓取的软件 I2C 和硬件 I2C 的时序波形。可以看出这是一个指定地址读的时序。我们看一下软件模拟的时序和硬件生成的时序,有什么异同。

软件 I2C 的波形

硬件 I2C 的波形

首先,从引脚电平变化趋势上看,这两个波形都是一样的,对应的数据也都是一样的。

然后,从时钟线的规整程序上看,硬件 I2C 的波形会更加规整,这里硬件 I2C,每个时钟的周期、占空比都非常一致;而软件 I2C 这里,由于操作引脚之后,都加了延时,这个延时有时候加的多,有时候加的少,所以软件时序的时钟周期、占空比可能不规整,不过由于 I2C 是同步时序,这些不规整也没有影响。

然后还有就是之前说了,SCL 低电平写,高电平读。虽然整个电平的任意时候都可以读写,但是一般要求保证尽早的原则,所以可以直接认为是 SCL 下降沿写,上升沿读,这里看一下,软件 I2C,在下降沿之后,因为操作端口之后有一些延时,所以这里等了一会,才进行写入操作,后面的写入,也是等了一会。但在硬件这里,数据写入,都是紧贴下降沿的,SCL 下降沿,SDA 立马就切换数据了,后面也是这样的。在读的时候,这里虽然绿线画在高电平中间了,但实际上读的时刻也是紧贴上升沿进行的,之后在应答结束时刻就更明显了,从机在 SCL 下降沿立刻释放了 SDA,但是软件 I2C 的主机,过了一会儿才变换数据,所以这里就出现了一个短暂的高电平。而硬件 I2C 呢,应答结束后,SCL 下降沿,从机立刻释放 SDA,同时主机也立刻拉低 SDA,所以就出现了一个小尖峰,那在后面,大家也可以同样进行对比,硬件操作的 I2C,包括上面这后面,有些是从机的硬件操作的,这些 SDA 的数据变化都是在 SCL 下降沿进行的,而这些软件操作的 I2C,波形可能就不是那么标准了。当然,还是因为 I2C 同步时序的原因,这些不标准的波形也完成不影响通信,这也正是同步时序的好处,可以容忍不标准的波形,那这些就是软件和硬件波形的对比,大家作为扩展部分了解一下。

那本小节,有关硬件 I2C 的介绍,就差不多了。最后我们来看一下手册,本小节的硬件 I2C 的知识点,对应手册的第 24 章,I2C 接口。

这里前面是一些简介和主要特点,基本都介绍过了,大家可以再看看;之后就是模式选择,因为这个硬件 I2C 是基于多主机的模型来设计的,所以这里就有从发送器、从接收器、主发送器、主接收器这 4 种模式,下面写的是该模块默认的工作于从模式,接口在生成起始条件后自动地从从模式切换到主模式,当仲裁丢失或产生停止信号时,则从主模式切换到从模式,允许多主机功能。之后是通信流,就是 I2C 协议的介绍。之后就依次是对从模式和主模式的介绍了,其中,从模式包括从发送器和从接收器的流程,这里从模式我们不做要求,主要是主模式,有一些整体的介绍,有一些细致的内容可能没介绍到,大家可以再看看,之后就是主发送器和主接收器的流程,这个是写程序的依据,得仔细分析一下。接着下面,错误条件,其中包括总线错误、应答错误、仲裁丢失、过载/欠载错误这些,就是当时序没有按照正常的情况进行,就会产生这些错误标志位,进一步可以触发中断,这是硬件的错误处理机制,至于具体解释,可以再看看手册。然后下面,SMBus,就是兼容的另外一种总线,这个暂时可以不看。之后 DMA 请求,当进行多字节数据收发时,可以配合 DMA,提高效率,这个需要用的话可以看看手册。然后包错误校验,这时硬件自带的 CRC 校验功能,需要的话也可以看看手册。之后 I2C 中断,就是发生一些事件或错误时,置标志位,这个标志为可以申请中断,总共有表里这么多的事件可以触发中断,需要的话可以开启对应的中断。那最后,就是 I2C 的寄存器了。 控制寄存器:这里面的位,都是控制硬件电路运行的。 自身地址寄存器:可以设置从模式下 STM32 自身的从机地址。 自身地址寄存器2:可以设置双地址模式下的自身从机地址。这个自身地址我们暂时用不到。 数据寄存器:在进行数据收发的时候,就是通过数据寄存器来进行的。数据寄存器背后,还有个移位寄存器进行配合,这个是通信的核心部分,之前都介绍过。 状态寄存器:里面存的就是一些标志位,标志电路状态,这些可以再看看。 时钟控制寄存器:用来确定通信的 SCL 时钟频率的,包括时钟的占空比和时钟分频系数,这里会有一些公式,但是我们有库函数,库函数会自动帮我们计算这些参数,所以这些公式就不需要我们再了解了。 TRISE 寄存器:这个不用了解。 然后最后,就是寄存器的总表了,手册里就是这些内容。那到这里,本小节的内容就全部结束了,我们下一小节来学习,硬件 I2C 的代码部分。

7. 配合 SPI 通信的外设

7.1 W25Q64 简介

这次我们学习的是使用了 SPI 通信的芯片之一,W25Q64。

这里我整体的介绍一下这个芯片,看完这个整体的介绍,相信你对这个芯片就有了大致的印象了。之后我们还是过一遍手册,把里面的一些重点和细节问题,再给大家介绍一下。这样,再和 SPI 的知识点结合起来,我们就可以开始写程序,来读写这个芯片了。

好,那我们先看一下 W25Q64 的简介部分。

W25Qxx 系列是一种低成本、小型化、使用简单的非易失性存储器。常应用于数据存储、字库存储、固件程序存储等场景。

W25Qxx 系列包含了多种型号,其中这个 xx 是一个数字,比如下面写的,40、80、16、32、64 等等,不同的数字,表示了这个芯片不同的存储容量。同一个系列的这些不同型号,他们的操作方法都是一样的,只是容量不同而已。那我们本套件使用的是 W25Q64 这个型号,它的容量是 64 Mbit,也就是 8 MByte,如果你觉得这个容量太大了,也可以换成 W25Q32、16 等等。更换之后,我们的硬件电路和底层驱动程序,都不需要更改,所以,我们学会了其中一种型号,再应用同系列的其他型号,就很容易上手了。低成本,也就是说这个芯片一般也就几块钱,它能提供的存储容量,可以看出,都是在 MB 的级别。这个容量级别,在手机电脑领域,是很小的,但在嵌入式领域,还是非常大的。当然 51 单片机还有一款比较经典的存储芯片,就是 AT24C02,它的容量,一般是 KB 级别的,那当然,相比之下,AT24C 系列的芯片就更便宜一些了。小型化,这个大家都能看出来,它就是一个 8 脚的小芯片,在电路板中比较省空间。使用简单,这就靠的是 SPI 协议的设计了,这个芯片是使用的 SPI 串行通信,通信引脚比较少,协议也很简单,那这个芯片的硬件接线也不麻烦,就 VCC、GND 接上电,剩下的全都可以接 GPIO,基本不需要其他电路,这就是这个芯片的使用,还是比较简单的。那最后,我们还可以看出,这个芯片是一种非易失性存储器。存储器分为易失性存储器和非易失性存储器。易失性存储器一般就是 SRAM、DRAM 等,非易失性存储器一般就是 E2PROM、Flash 等。他们最主要的区别,简而言之,就是存储的数据是否是掉电不丢失,那非易失性存储器就是数据不容易失去的存储器,也就是数据掉电不丢失,所以存储在这个芯片里的数据,在断电重启后,数据仍然保持原样。既然数据是掉电不丢失的,那你就可以根据这个特性来做一下应用了。比如说,常应用于数据存储,比如你 STM32 有一些参数、或者采集的数据,想要掉电不丢失的保存,那就可以写入到这个芯片里来,然后字库存储,这个可以应用到一些显示屏上,比如 OLED 显示屏或者 LCD 液晶屏,你如果想在屏幕上显示汉字,就得把汉字的点阵数据存起来。当然简单的方法是,把字库直接存在 STM32 内部,这样适合少量汉字显示的情况,如果汉字非常多,再直接存在 STM32 中,就不合适了。所以我们可以用这个芯片来存储汉字字库的点阵数据,在显示某个汉字前,先读取芯片查询字库,再在显示屏上显示对应的点阵数据,这样就能让显示屏任意显示中文了。然后固件程序存储,这个就相当于直接把程序文件下载到外挂芯片里,需要执行程序的时候,直接读取外挂芯片的程序文件来执行,这就是 XIP(eXecute In Place),就地执行。比如我们电脑里的 BIOS 固件,就可以存储在 W25Q 系列的芯片里。

好,这些就是这个芯片的特性和用途了。当然我们本节,只使用它最简单的数据存储功能。

存储介质:Nor Flash(闪存)

Flash 就是闪存存储器,像我们 STM32 的程序存储器、U盘、电脑里的固态硬盘等,使用的都是 Flash 闪存,闪存分为 Nor Flash 和 Nand Flash。两者各有优势和劣势,适用领域不同,这个感兴趣的话可以百度了解一下,就不展开说了。大家知道我们这个芯片是 Nor Flash 即可。

时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)

我们这个芯片使用的是 SPI 通信。其中 SPI 的 SCK 线,就是时钟线,这个时钟线的最大频率,是 80 MHz,这个频率相比较 STM32,是非常快了,所以我们之后写程序的时候,翻转引脚就不用再加延时了。即使不延时,这个 GPIO 的翻转频率也不可能达到 80 MHz,所以可以放心使用。 然后后面这还有两个频率,分别是 160 MHz,这个是双重 SPI 模式等效的频率;320 MHz,这个是四重 SPI 模式等效的频率。这个双重 SPI 和四重 SPI,大家了解一下即可,我们本节不会用到。那他们是什么意思呢?就是我们之前说的,MOSI 用于发送,MISO 用于接收,是全双工通信,在只发或只收时有资源浪费,但是这个 W25Q 芯片的厂商不忍心浪费,所以就对 SPI 做出了一些改进,就是,我在发的时候,我可以同时用 MOSI 和 MISO 发送,在收的时候,也可以同时用 MOSI 和 MISO 接收,MOSI 和 MISO 同时兼具发送和接收的功能。一个 SCK 时钟,我同时发送或接收 2 位数据,这就是双重 SPI 模式;那你一个时钟收发两位,相比较 1 位 1 位的普通 SPI,数据传输率就是 2 倍了。所以这里写的是,在双重 SPI 模式下,等效的时钟频率就是 80 MHz 的二倍,就是 160 MHz,但实际的 SCK 频率,最大还是 80 MHz,只是 1 个时钟发 2 位而已。然后四重 SPI 模式,很显然,就是一个时钟发送或接收 4 位了,等效的频率就是 80*4 = 320 MHz,在我们这个芯片里,除了 SPI 通信引脚,还有两个引脚,一个是 WP 写保护,另一个是 HOLD,这两个引脚,如果不需要的话,也可以拉过来,充当数据传输引脚,加上 MOSI 和 MISO,这就可以 4 个数据位同时收发了,这就是四重 SPI。其实这就有点并行传输的意思了,串行是根据时钟,一位一位的发送,并行是一个时钟,8 位同时发送,所以这个四重 SPI 模式,其实就是 4 位并行的模式,这个大概了解一下就行,我们暂时用不到。

那最后,我们来看一下不同型号对应的存储容量(24位地址): W25Q40: 4Mbit / 512KByte W25Q80: 8Mbit / 1MByte W25Q16: 16Mbit / 2MByte W25Q32: 32Mbit / 4MByte W25Q64: 64Mbit / 8MByte W25Q128: 128Mbit / 16MByte W25Q256: 256Mbit / 32MByte

W25Q 系列,总共有这么多型号。容量我们习惯以字节为单位,一个字节对应 8 个二进制位。 型号后面的数值,就是 对应数值的 M 的容量,单位都是 bit。对应到常用的以字节的单位,就是都除 8,所以这个型号后面的数值,除 8,就是存储容量。当然这个 40 和 80 是例外,得看成 04 和 08 这样才对。 这个芯片使用的是 24 位的地址,24 位地址,是 3 个字节。因为我们在进行读写的时候,肯定得把每个字节都分配一个地址,这样才能找到它们。上一小节讲时序的时候也提到过,这里在指定地址时,需要一次性指定 3 个字节,24 位的地址。然后我们可以用计算器算一下,24 位的地址最大能分配多少个字节呢?这里 224/1024/1024 = 16 MB,所以 24 位地址的最大寻址空间是 16 MB。所以 W25Q40 到 Q128,使用 3 字节 24 位的地址都是足够的。但是 W25Q256 就比较尴尬了,24 位地址,对于 32 MB 来说,是不够的,所以这最后一个型号比较特殊。根据手册里描述,W25Q256 分为 3 字节地址模式和 4 字节地址模式,在 3 字节地址模式下,只能读写前 16 MB 的数据,后面 16 MB,3 个字节的地址够不着;要想读写到所有存储单元,可以进入 4 字节地址的模式,这样就行了。所以如果你需要使用 W25Q256 这个型号,要注意一下这个问题。

好,到这里,我们这个 W25Q 系列芯片的简介就讲完了。

接下来,我们就来看一下这个芯片的硬件电路。当我们拿到这个 8 脚的芯片后,怎么把它和 STM32 连接在一起呢,我们看一下。 左边这个图就是这个芯片的引脚定义,右边这个表就是每个引脚定义的功能了。

首先看一下引脚定义。

8 脚 VCC、4 脚 GND,这里是电源供电引脚,供电电压是 2.7~3.6 V,是一个典型的 3.3V 供电设备,不能直接接入 5V 电压。1 号脚 CS,这个 CS 左边画了个斜杠,代表是低电平有效。或者原理图中 CS 上面画了个横线,也是低电平有效。那这里,CS 对应之前我们讲 SPI 的名称就是 SS,意思是 SPI 的片选引脚。6 号脚 CLK对应就是 SCK,是 SPI 时钟线。5 号脚 DI,对应 MOSI,是 SPI 主机输出从机输入。2 号脚 DO,对应 MISO,是 SPI 主机输入从机输出。

这 4 个引脚,就是 SPI 通信的 4 个引脚。

然后这个芯片还有两个引脚:3 号脚 WP(Write Protect),它的意思是写保护,配合内部的寄存器配置,可以实现硬件的写保护。写保护低电平有效,WP 接低电平,保护住,不让写;WP 接高电平,不保护,可以写。最后 7 号脚 HOLD,意思就是数据保持,低电平有效,这个用的不多,了解一下。就是如果你在进行正常读写时突然产生中断,然后想用 SPI 通信线去操纵其他器件,这时如果把 CS 置回高电平,那时序就终止了,但如果你又不想终止总线,又想操作其他器件,这就可以 HOLD 引脚置低电平,这样芯片就 HOLD 住了,芯片释放总线,但是芯片时序也不会终止,它会记住当前的状态,当你操作完其他器件时,可以回过来,HOLD 置回高电平,然后继续 HOLD 之前的时序,相当于 SPI 总线进了一次中断,并且在中断里,还可以用 SPI 干别的事情,这就是 HOLD 的功能。

然后最后我们注意到,这个 DI、DO、WP 和 HOLD 旁边都有括号,写了 IO0、IO1、IO2、IO3,这个就对应我们刚才这里说的,双重 SPI 和四重 SPI。如果是普通的 SPI 模式,那括号里的都不用看;如果是双重 SPI,那 DI 和 DO 就变成 IO0 和 IO1,也就是数据同时收和同时发的 2 个数据位;如果是四重 SPI,那就再加上,WP 当作 IO2,HOLD 当作 IO3,这 4 个引脚都作为数据收发引脚,1 个时钟,4 个数据位。这个了解一下即可,我们暂时不用。

好,那这些就是芯片的所有引脚功能定义了。之后看一下模块原理图。

下面这个图,是我们这个小模块的原理图。 这个 U1,就是 W25Qxx 的芯片。J1 是一个 6 脚的排针。

然后芯片的 VCC,电源正极,通过 VCC 引脚标号,接到排针 6 号脚。芯片 GND,电源负极,通过 GND 标号,接到排针 3 号脚,然后芯片 SPI 通信的 4 个脚,都直接通过排针引出来,就行了。之后 HOLD 和 WP,这两个都是直接接到了 VCC,低电平有效,那都接到 VCC,就是这两个功能,我们都不用。然后 C1,直接接到 VCC 和 GND,显然是一个电源滤波。R1 和 D1 也是直接接到 VCC 和 GND,显然是一个电源指示灯,通电就亮。那这些,就是这个芯片的硬件电路了,接线整体上也是非常的简洁。

VCC、GND 接 3.3V 电源;SPI 通信线直接接到 STM32 的 SPI 通信引脚;HOLD 和 WP 如果需要用的话,就接到 STM32 的 GPIO,不需要用,就直接接 VCC;然后电源指示灯和电源滤波,这些都看需求加就行。这就是硬件接线。

那硬件电路看完,我们再看一下 W25Q64 的框图。 这个框图,其实每个芯片都有,也是我们理解每个芯片,最直观的方式。所以框图,需要我们仔细分析,我们看一下。

首先,右上角这一大块,描述的是存储器的规划示意图。我们这个 W25Q64,容量是 8 MB,如果不进行划分,而只按照一整块来使用的话,那这一整块的容量就太大了,不利于管理;而且后续,我们涉及到 Flash 擦除,或者写入的时候,都会有个基本单元,我们得以这个基本单元为单位进行,所以这里,这一整块大蛋糕,8 MB 的存储空间,就有必要进行一些合理的划分。那常见的划分方式就是:一整块存储空间,先划分为若干的块 Block,其中每一块再划分为若干的扇区 Sector,对于每个扇区,内部又可以分成很多页 Page。那我们看一下 W25Q64 是怎么划分的呢?首先,这一整个矩形空间里,是所有的存储器,存储器以字节为单位,每个字节都有唯一的地址。之前说了,W25Q64 的地址宽度是 24 位,3 个字节,所以可以看到,左下角,第一个字节它的地址是 00 00 00 h,h 代表 16 进制,之后的空间,地址依次自增,直到最后一个字节,地址是 7F FF FF h,那最后一个字节为啥是 7F 开头,不是 FF 开头呢,因为 24 位地址,最大寻址范围是 16 MB,我们这个芯片只有 8 MB,所以地址空间,我们只用了一半,8 MB 的空间,排到最后一个字节,就是 7F FF FF,那这是整个地址空间,从 00 00 00 到 7F FF FF。然后在这整个空间里,我们以 64 KB 为一个基本单元,把它划分为若干的块 Block,从前往后,依次是块 0、块 1、块 2、等等等等,一直分到最后一块,那整块蛋糕是 8 MB,以 64 KB 为一块进行划分,最后分得的块数,就是 8 MB/64 KB = 128,所以这里可以分得 128 块,那块序号,就是块 0,一直到最后一个,是块 127;然后观察一下块内地址值的变化规律,比如块 0 的起始地址是 00 00 00,结束地址是 00 FF FF,之后,块 31,起始是 1F 00 00,结束是 1F FF FF,之后的都可以观察一下,可以发现,在每一块内,它的地址变化范围就是最低的 2 个字节,每个块的起始是 xx 00 00,结束是 xx FF FF,这是块内地址的变化规律,了解一下,好,到这里,这一块大蛋糕,我们就分好块了,64 KB 为一块,总共 128 块。之后看一下左边,这个示意图就是,我们还要再对每一块进行更细的划分,分为多个扇区 Sector,中间的虚线指向了右边的各个块,也就是告诉你,每一块里面,都是这个样子的,我们看一下,那在每个块里,它的起始地址是 xx 00 00,结束地址是 xx FF FF,在一块里,我们再以 4 KB 为一个单元,进行切分,一块是 64 KB,4 KB 一切,总共切得 64 KB/4 KB = 16 份,所以在每一块里,都可以分为扇区 0,一直到扇区 15,观察一下地址规律,可以发现,每个扇区内的地址范围是 xx x0 00,到 xx xF FF,这就是对每一块,再细分为 16 个扇区的分配方式,当然,地址划分,到扇区就结束了。但是当我们在写入数据时,还会有个更细的划分,这就是页 Page,页是对整个存储空间划分的,当然你也可以把它看作,在扇区里,再进行划分,都是一样,那页的大小呢,是 256 个字节,一个扇区是 4 KB,以 256 个字节划分,能分得 4 KB/256 B = 16 页;然后页的地址规律,我们也看一下,在这里,每一行就是一页,左边这里指了个箭头,写的是页地址的开始,右边这里也指了个箭头,写的是页地址的结束,在一页中,地址变化范围是 xx xx 00,到 xx xx FF,一页内的地址变化,仅限于地址的最低一个字节,好,这就是页的划分。那这个存储器的地址划分,我就介绍完了,这些划分的具体数值大家可以不用来记,但是我们需要记住的是,一整个存储空间,首先划分为若干块,对于每一块,又划分为若干扇区,然后对于整个空间,会划分很多很多页,每页 256 字节,这个我们需要记住,后面还会用到。好,然后我们来看左下角,这是 SPI 控制逻辑,也就是芯片内部进行地址锁存、数据读写等操作,都可以由控制逻辑来自动完成,这个不用我们操心,控制逻辑就是整个芯片的管理员,我们有什么事,只需要告诉这个管理员就行了,然后控制逻辑左边,就是 SPI 的通信引脚,有 WP、HOLD、CLK、CS、DI 和 DO,这些引脚,就和我们的主控芯片相连,主控芯片通过 SPI 协议,把指令和数据发给控制逻辑,控制逻辑就会自动去操作内部电路来完成我们想要的功能。然后继续看,控制逻辑上面有个状态寄存器,这个状态寄存器是比较重要的,比如芯片是否处于忙状态、是否写使能、是否写保护,都可以在这个状态寄存器里体现,这个我们等会看手册的时候再来分析。然后上面,是写控制逻辑,和外部的 WP 引脚相连,显然,这个是配合 WP 引脚实现硬件写保护的。然后继续,右边是一个高电压生成器,这个是配合 Flash 进行编程的,因为 Flash 是掉电不丢失的,如何实现掉电不丢失呢,比如你点亮一个 LED 表示 1,熄灭 LED 表示 0。但如果整个系统电都没有,那 1 和 0 就无从说起了,所以要想掉电不丢失,就要我们在存储器里,产生一些刻骨铭心的变化。比如一个 LED,我给它加很高的电压,那 LED 就烧坏了,我们用烧坏的 LED 表示 1,没烧坏的 LED 表示 0,然后再断电,烧坏的 LED 还是烧坏的,有电没电,它都是坏的,这个烧没烧坏的状态,不受有电还是没电的影响,所以它就是掉电不丢失的,那对于我们的非易失性存储器来说,也是一样,我们要让它产生即使断电也不会消失的状态,一般都需要一个比较高的电压去刺激它,所以这种掉电不丢失的存储器,一般都需要一个高压源,那这里,芯片内部集成了高电压发生器,所以就不需要我们再外接高电压了,比较方便,当然我这里只是举例简单描述一下掉电不丢失的存储原理,至于 Flash 的原理,大家可以再另行研究。然后继续看下面,这里是页地址锁存/计数器,然后下面还有个字节地址锁存/计数器,这两个地址锁存和计数器,就是用来指定地址的,我们通过 SPI,总共发过来 3 个字节的地址,因为 1 页是 256 字节,所以 1 页内的字节地址,就取决于最低一个字节,而高位的 2 个字节,就对应的是页地址,所以在这里,我们发的 3 个字节地址,前两个字节,会进到这个页地址锁存计数器里,最后一个字节,会进到这个字节地址锁存计数器里,然后,页地址通过这个写保护和行解码来选择我要操作哪一页;字节地址,通过这个列解码和 256 字节页缓存来进行指定字节的读写操作。那又因为我们这个地址锁存,都是有一个计数器的,所以这个地址指针,在读写之后,可以自动加 1,这样就可以很容易实现从指定地址开始,连续读写多个字节的目的了。那最后,右边这里,有个 256 字节的页缓存区,它其实是一个 256 字节的 RAM 存储器,这个稍微留个印象,等会儿还会提到,然后我们数据读写,就是通过这个 RAM 缓存区来进行的,我们写入数据,会先放到缓存区里,然后在时序结束后,芯片再将缓存区的数据复制到对应的 Flash 里进行永久保存。那为啥要弄个缓存区呢,我们直接往 Flash 里写不好吗?那这是因为,我们的 SPI 写入的频率是非常高的,而 Flash 的写入,由于需要掉电不丢失,留下刻骨铭心的印象,它就比较慢,所以这个芯片的设计思路就是,你写入的数据,我先放在页缓存区里存着,因为缓存区是 RAM,所以它的速度非常快,可以跟的上 SPI 总线的速度,但是这里有个小问题,就是这个缓存区只有 256 字节,所以写入的时序有个限制条件,就是写入的一个时序,连续写入的数据量,不能超过 256 字节,然后等你写完了,我芯片再慢慢的把数据从缓存区转移到 Flash 存储器里,那我数据从缓存区转到 Flash 里,需要一定的时间,所以在写入时序结束后,芯片会进入一段忙的状态,在这里,它就会有一条线,通往状态寄存器,给状态寄存器的 BUSY 位置 1,表示芯片当前正在搬砖呢,很忙,那在忙的时候,芯片就不会响应新的读写时序了,这就是写入的执行流程。然后我们读取数据呢,虽然这里画的应该也会通过缓存区来读取,但是由于读取,只是看一下电路的状态就行了,它基本不花时间,所以读取的限制就很少了,速度也非常快。

好,那到这里,我们这个芯片的框图就看完了。这个框图,有几个重点部分,第一个是,这整个 Flash 的空间划分,会划分为块、扇区和页;第二个是 SPI 控制逻辑,它就是整个芯片的管理员,执行指令、读写数据都靠它;第三个是状态寄存器,它和忙状态、写使能、写保护等功能有关;第四个是 256 字节的页缓存,它会对一次性写入的数据量,产生限制。这就是这整个框图。

那最后,我们来看一下 Flash 操作的注意事项。这里列出了 Flash 写入和读取的要求,步骤还是比较繁琐的。

那你可能会有些疑惑,不就是个存储器么,我直接指定地址写或者指定地址读,然后它直接给我把数据存在对应的存储单元里不就行了嘛,为啥还要搞这么多的注意事项呢。其实这是因为 Flash,作为一种掉电不丢失的存储器,为了保证掉电不丢失这个特性,同时还要保证存储容量足够大、成本足够低,所以,Flash 存储器会在其他地方,比如操作的便捷性等做出一些妥协和让步,Flash 的写入和读取并不像 RAM 那么简单直接。RAM 是指哪打哪,想在哪写就在哪写,想写多少就写多少,并且 RAM 是可以覆盖写入的,比如原来 RAM 里有个数据 0xAA,之后我直接再写入一个新的数据 0x55,那 RAM 的数据就变成 0x55 了,这个特性是非常好的,但是 Flash 并没有这个特性。总之,Flash 的读写有很多要求,其中写入的要求是非常多的,需要我们掌握,读取的要求就比较少了。还是那个原因,因为读取,只是看一下电路的状态,不对电路做出实质性的改变,所以读取一般都比较快,而且没有什么限制。那我们看一下 Flash 写入操作时,需要注意些什么呢?

写入操作时:

写入操作前,必须先进行写使能

这个是一种保护措施,防止你误操作的,就像我们使用手机一样,先解锁再操作。这样可以防止手机在你裤兜里到处点点点,对吧。写使能的话,我们就使用 SPI,发送一个写使能的指令,就可以完成了,这个还是比较好理解的。

每个数据位只能由1改写为0,不能由0改写为1

这个意思就是说,Flash 并没有像 RAM 那样的,直接完全覆盖改写的能力。比如,在某一个字节的存储单元,存储了 0xAA 这个数据,对应的二进制位就是 1010 1010,如果我直接再次在这个存储单元写入一个新的数据,比如我再次写入一个 0x55,那写完之后,这个存储单元里存的是 0x55 吗?实际上并不是,因为 0x55 的二进制是 0101 0101,当这个 0101 0101 要覆盖原来的 1010 1010 时,就会受到这里第二条规定的限制,每个数据位只能由1改写为0,不能由0改写为1。你要问为啥会有这个限制,那只能说是成本原因,或者技术原因了。所以这里,写入 0101 0101 之后,依次来看,最高位由原来的 1 改写为 0,是可以的,所以写入之后,新的最高位就是 0;但是第二位,原来是 0,现在我想改写为 1,这是不行的,所以写入之后,新的第二位,仍然是 0;之后第三位,1 改写为 0,可以,结果为 0;第四位,0 改写为 1,不可以,结果仍然是 0;那以这个规律进行下去,0xAA 再覆盖写入 0x55 之后,这个存储单元最终的数据是什么,0x00,也就是 8 位全为 0。这就出现问题了对吧,我们明明写入了 0x55,但是它实际存储的却是 0x00,这不就出错了么。所以,为了弥补这个只能 1 改为 0,不能 0 改为 1 的缺陷,我们就引出了第三条规定。

写入数据前必须先擦除,擦除后,所有数据位变为1

在这里,Flash 是有一个擦除的概念的。擦除会有专门的擦除电路进行,我们只要给它发送擦除的指令就行了,那通过擦除电路擦除之后呢,所有的数据位都变成 1。这样,我们是不是就可以弥补第二条限制的缺陷了,当我们写入一个数据之前,无论原来存的是什么,我直接给它擦除掉,擦除之后,所有的位变成 1,也就是十六进制 FF。这样,我无论再写入什么样的数据,就都可以正确的写入了。比如 0x55,0101 0101,最高位由 1 改写为 0,为 0;第二位,写入的是 1,虽然不能改写为 1,但是擦除之后,它原来就已经是 1 了,所以这时写入 1 的数据位就不会出错了;之后也是同理,1 和 0,都和我们写入的数据是对应的,写入 0x55 之后,数据单元存的就是 0x55,没问题。那总结一下就是,Flash 中数据位为 1 的数据,拥有单向改成 0 的权利,一旦改写为 0 之后,就不能反悔再改写为 1 了。要想反悔,就必须得先擦除,所有的位先统一都变成 1,然后再重新来过,这就是 Flash 改写的特性和写入数据前必须擦除的原因。如果你说,我非不擦除,直接改写,这样的操作可以执行,但是存储的数据,极有可能是错的,这个注意一下。那擦除之后,所有的位变成 1,就是十六进制的 FF,所以有时候你读取 Flash,会发现数据全是 FF,那就说明,这一段有可能是擦除之后,还没有写入数据的空白空间,在 Flash 中 FF 代表空白,而不是 00。好,那这个改写和擦除的注意事项 ,我们就了解了

擦除必须按最小擦除单元进行

这个应该也是为了成本而做出的妥协。就是说,你写入前要进行擦除嘛,这我知道,所以如果我想在 00 这个地址下写入数据,那我就先把 00 地址擦除,再写入数据到 00 地址,不就行了嘛。但是,这个方案有个问题:Flash的擦除有最小擦除单元的限制,你不能指定某一个字节去擦除,要擦,就得一大片一起擦,一大片是多大一片呢?那在我们这个芯片里,你可以选择,整个芯片擦除,也可以选择,按块擦除,或者按扇区擦除,然后再小,就没有了,所以最小的擦除单元,就是一个扇区。刚才我们看了,一个扇区是 4 KB,就是 4096 个字节,所以你擦除,最少,就得 4096 个字节一起擦。那你说,我只想擦除某一个字节怎么办呢?这没办法,你只能把那个字节所在扇区的 4096 个字节全都擦掉。那你又说,这个扇区其他的地方,我还存的有数据怎么办呢?这也没办法,要想不丢失数据,你只能先把 4096 个字节都读出来,再把 4096 个字节的扇区擦掉,改写完读出来的数据后,再把 4096 个字节全都写回去,这感觉是不是挺麻烦的,但是如果你确实就想单独改写某一个字节,那只能这样来操作。当然实际情况下,我们还有别的方法可以优化一下这个流程。比如,上电后,我先把 Flash 的数据都出来,放到 RAM 里,当有数据变动时,我再统一把数据备份到 Flash 里;或者,我把使用频繁的扇区,放在 RAM 里,当使用频率降低时,我再把整个扇区备份到 Flash 里;或者,如果你数据量确实非常少,只想存几个字节的参数就行了,那直接一个字节占一个扇区,不就行了嘛,尽显奢靡之风。总结下来,为了弥补擦除存在最小单元的缺点,我们需要在程序逻辑上,做出一些设计。

连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入

这个意思就是说,你在写入的时候,一次性不能写太多了,一个写入时序,最多只能写一页的数据,也就是 256 字节。为什么有这个限制呢?这是因为页缓存区它只有 256 字节。为什么有缓存区呢?这是因为,Flash 的写入,太慢了,跟不上 SPI 的频率,所以写入的数据,会先放在 RAM 里暂存,等时序结束后,芯片再慢慢的把数据写入到 Flash 里,所以,这里会有限制:每个时序,最多写入一页的数据,你再写多,缓存区存不下了,如果你非要写,那超过页尾位置的数据,会回到页首覆盖写入。另外,我们这个页缓存区,是和 Flash 的页对应的,你必须得,从页起始位置开始,才能最大写入 256 字节,如果你从页中间的地址开始写,那写到页尾时,这个地址就会跳回到页首,这会导致地址错乱。所以我们在进行多字节写入时,一定要注意,这个地址范围,不能跨越页的边沿,否则会地址错乱,那这是这一点的注意事项。

写入操作结束后,芯片进入忙状态,不响应新的读写操作

这个应该好理解,刚才说了,我们的写入操作,都是对缓存区进行的。等时序结束后,芯片还要搬砖一段时间,所以每次写入操作后,都有一段时间的忙状态,在这个状态下,我们不要进行新的读写操作,否则芯片是不会响应我们的,要想知道芯片什么时候结束忙状态了。我们可以使用读取状态寄存器的指令,看一下状态寄存器的 BUSY 位是否为 1,BUSY 位为 0 时,芯片就不忙了,我们再进行操作。另外注意,这个写入操作,包括上面的擦除,在发出擦除指令后,芯片也会进入忙状态,我们也得等待忙状态结束后,才能进行后续操作。

好,那以上就是 Flash 写入操作的注意事项了。这些注意事项,大家都得牢记,否则,我们下一小节的程序逻辑,你就不好理解了。

读取操作时:

直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取。

这个就相对宽松很多了。无需使能,就是写入的第一条操作,不需要;无需额外操作,就是写入的 2~4 条,和读取无关;没有页的限制,也就是写入的第五条操作,连续读取多个字节时,想读多少就读多少,不用担心地址错位或者覆盖的问题;读取操作结束后不会进入忙状态,但不能在忙状态时读取,就是读取之后,芯片不会忙,可以立刻开始下一条指令,但是不能在忙状态时读取,其实是写入的第六条操作的限制。因为写入操作,进入忙状态后,不会响应新的读写操作,所以我们在读取之前,也要注意一下,芯片是否处于忙状态,等不忙的时候,再读,就可以了。

好,到这里,我们这个注意事项就讲完了。看完之后,是不是觉得,有点麻烦,但是,Flash 这种非易失性存储器,目前的市场竞争力还是非常大的,尽管它有这么多不方便,但是这些不方便可以用软件来弥补,而它的优点,是其他存储器比不了的。比如容量大,价格低,速度,相比较 RAM 是慢,但是在非易失性存储器中,Flash 是非常快的,就像它的名字一样,Flash 闪存,如闪电一般的快。

到这里,相信你对我们这个芯片也有了大概的了解了。接下来我们还是看一下手册,在手册里,还有几个重要的知识点要说一下。

首先是这个芯片的概述和特性,特性中有一些关键的指标,可以把它当成广告页来看,看看它有什么优势,这个大家自己再看看。

然后下面是芯片的引脚定义,这个我们刚才介绍过,包括接线图,也是非常简单直接的。

然后是对芯片每个引脚的描述,这个都介绍过,还有不清楚的地方,可以再看看手册。

之后是芯片的系统框图,这个比较重要,刚才也给大家详细分析过。

接下来是 SPI 的操作,比如标准 SPI、双重 SPI、四重 SPI,这些大家可以再看看,当然双重 SPI 和四重 SPI,大家可以先不用管。

然后是写保护的逻辑,我们暂时也不用写保护,大家可以先随便看看。

然后从 11 章开始,还有提两个很重要的东西:第一个是状态寄存器,第二个是 SPI 指令集。

先看一下状态寄存器,其实这个状态寄存器应该叫控制及状态寄存器,它里面也有一些控制位,当然简单称呼,就直接叫状态寄存器了。这里状态寄存器总共有两个,状态寄存器 1 和状态寄存器 2。其中比较重要的是状态寄存器 1,状态寄存器 1 里,又有两个位,是比较重要的,这两个位在手册里我标出来了。第一位是我们刚才提到多次的 BUSY,下面有些描述,大概意思就是,当设备正在执行页编程,页编程就是写入数据;然后扇区擦除,块擦除,整片擦除;或者写状态寄存器指令时,BUSY 位置 1,在这期间,设备将会忽略进一步的指令,当然除了读状态寄存器和擦除挂起指令。然后是当编程、擦除、写状态寄存器指令结束后,BUSY 清零来指示设备准备好了,这就是 BUSY 位。然后下一位,写使能锁存位 WEL,在执行完写使能指令后,WEL 置 1,代表芯片可以进行写入操作了,当设备写失能时,WEL 位清 0,那什么时候处于写失能状态呢?一是,上电后,芯片默认写失能;二是在执行完这些指令之后,其中包括我们发送了写失能指令,让它写失能,WEL 就 = 0;其次,页编程、扇区擦除等等,这些写入操作之后,WEL 会 = 0.这表明,当我们先写使能,再执行写入数据操作后,不需要再手动进行写失能了,因为写入操作后会顺便帮我们写失能,相当于有个顺手关门的操作。同时这也表明,我们在进行任何写入操作前,都得来一遍写使能,一个写使能,只能保证后续的一条写指令可以执行,这个了解一下。好,有关状态寄存器,我主要提这两位,其他的位,大家再自行查看,主要就是一些写保护的功能,需要的话,再来看看即可。 然后下面是状态寄存器的示意图,其中状态寄存器 1,我们主要掌握最低的两位,其他的这些位,暂时不用管。之后是一个写保护的配置表,你想保护哪些存储单元,就按照这个表来操作,好,之后,我们进入下一个重要部分,就是指令集。指令,英文单词是 Instruction,了解一下。之后是一个表,写的是芯片 ID 号。其中,厂商 ID,是 EF;设备 ID,如果你使用 ABh,90h 这两个指令来读,就是 16;如果使用 9Fh 指令来读,就是 4017,这个我们写程序的时候,首先读取 ID,验证一下 SPI 是不是可行,到时候就参考这个表来验证即可。 然后下面一个表,就是 SPI 的指令集了。这个指令比较多,我们目前先只学习这里标记的指令就行了。

其中第一个,是写使能(Write Enable),指令码是 06,要想发送写使能指令,我们就利用上一小节学习的 SPI 来操作,先起始,然后交换一个字节,第一个字节是发送方向,发送 0x06 指令,就行了,这个指令后续不需要跟数据,所以直接停止,这就是一条写使能的指令。然后下面,写失能的指令(Write Disable),也是一样,起始,交换字节发送指令码 04,终止,就完成了。之后,读状态寄存器 1(Read Status Register-1),我们需要起始,交换字节发送指令码 05,这是读指令,所以后面有数据,我们要继续交换字节,通过交换读取一个字节,那这个字节,就是状态寄存器的 S7~S0,当然如果继续交换接收的话,那芯片会不断的输出状态寄存器 1,其中 S0 是 BUSY 位,S1 是 WEL 位,这条指令,主要是用来查看忙状态的。接着下一个指令,是页编程(Page Program),这个页编程就是写数据,只是它有个 256 字节页大小的限制,所以这里叫页编程,操作流程是:起始,交换字节发送指令 02,然后继续交换发送地址的 23~16 位、15~8 位、7~0 位,这 3 个字节用来指定地址,再之后,我们就可以写入数据 D7~D0 了,这个数据写入到刚才指定的地址下,如果继续交换写入的话,后续的字节就从起始地址开始依次存储,但是要注意页的限制,这就是页编程的指令。之后这里几条,就是擦除指令,其中包括,按 64 KB 的块擦除(Block Erase(64KB)),按 32 KB 的块擦除(Block Erase(32KB)),按 4 KB 的扇区擦除(Sector Erase(4KB))和整片擦除(Chip Erase),我们想少擦点,那就主要用扇区擦除即可,使用方法是:起始,交换字节发送指令 20,之后再交换发送 3 个字节的地址,终止,发送之后,这个指定地址所在的扇区就会被整个擦除,当然这个地址是精确到某个字节的,我们一般会把这个地址对齐到扇区的首地址,这样表示擦除一整个扇区,意思更加明确。之后,下面这里还有几个读取 ID 号的指令,我们主要用这个 JEDEC ID,操作流程是,起始,交换发送 9F,随后连续交换读取 3 个字节,终止,其中第一个字节是厂商 ID,后两个字节是设备 ID,这是读取 ID 号的指令。然后继续,最后一个要看的指令,是读取数据(Read Data),操作流程是,起始,交换发送指令 03,之后交换发送 3 个字节的地址,再之后,就可以交换读取了,这个数据就是这个地址下的数据,如果继续交换读取,后面数据就是从指定地址开始依次读存储的数据,这个读取没有页的限制,所以直接叫 Read Data,想怎么读都可以。

好,以上就是这芯片的指令集了。我们下一小节编程写时序的时候,就得看这个指令集和操作流程,所以这一块需要多了解了解。

之后呢,这个手册后续部分,就是对每条指令的详细解释了,包括时序图也画出来了,这个对每个指令,都有详细的描述,包括我们之前提到的各种注意事项,手册里也都有提,这里每条指令的详细解释我就不带大家一起看了,大家听完我之前的介绍,再好好看看,应该也是可以理解的。那下面的时序图可以看到,和我们之前介绍的基本一致,有几个区别呢,就是这里的 DO,按理说,从机应该通过 DO 把它回传的数据发过来,但是这是一个写入的指令,确实不用回传数据,所以这里 DO 始终保持高阻态,这也可以;然后 SCL 这里有个 Mode 0,又画了个虚线,写着 Mode3,这个意思是,这个芯片可以兼容 SPI 的模式 0 和模式 3,为什么能兼容呢?可以对比一下模式 0 和 模式 3,可以发现它们都是下降沿移出,上升沿移入,只是相位不一样,我们可以规定,在 SS 下降沿或者 SCK 的第一个下降沿,都输出 B7,然后后续,移入数据,都是在 SCK 的上升沿开始,这样是不是就可以兼容模式 0 和 模式 3 了,所以手册里就画了个虚线,说你模式 0 和模式 3 都可以用,我可以兼容,当然我们一般选取实线的模式 0 就可以了,所以虚线部分我们可以忽略不看,这就是时序的一些区别。

然后剩下的这些,都是指令的具体介绍和注意事项,大家可以自己慢慢看。最后指令介绍完成之后,就是电气特性的一些表,比如 12.2 给出了这个芯片的供电电压范围,供电要在这个范围内才行。

之后还是一些表,然后表里有一些参数可能会用到,就是执行编程和擦除的时间,比如我执行页编程的时间,它的典型时间是 0.7 ms,最大时间是 3 ms;扇区擦除的时间,典型值 30 ms,之后块擦除,典型值是 120 和 150 ms,最后一个整片擦除,这个时间最长,典型值 15 s,最大 30 s。所以看出,这个 Flash 芯片的写入时间一般情况下,大概处于一个 ms 的时间级别,然后后面就是这些时序的时间要求了。

最后是一些封装的尺寸信息,这就是这个手册的内容。那到这里,本小节就结束了。我们下一小节,开始写代码,实现程序功能。

7.2 SPI 外设简介

本小节我们来继续学习 SPI 通信,这次我们学习的是硬件 SPI,跟之前 I2C 的思路一样,大家应该已经有经验了。

软件 SPI 就是我们用代码手动翻转电平,来实现时序;硬件 SPI 就是使用 STM32 内部的 SPI 外设,来实现时序。两种实现方法各有优势: 软件实现主打的是方便灵活,硬件实现主打的是高性能、节省软件资源。

那看一下 SPI 外设简介。

STM32 内部集成了硬件 SPI 收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻 CPU 的负担

就是硬件电路自动生成时序,不用我们自己手动翻转电平,节省软件资源。

然后下面这些是 STM32 内部 SPI 外设的一些功能和技术参数。

其实手册里介绍这些功能还是非常繁杂的,这是因为硬件电路不像软件那么灵活,硬件电路一旦设计出来,它的功能基本上就定死了,之后只能通过一些开关电路、数据选择器等等来微调电路的运行,不像软件那样,我们缺少功能,只需要去 copy 代码就行。所以 STM32 设计时,就要考虑最全面的应用场景,把各种可能的结构都设计出来,放在那,以免你用的时候找不到。

那这样,就会导致外设电路的结构和知识点非常多,而且有很多功能,我们基本上很少用到,所以 STM32 我们要使用主线+分支的学习方法。我们先把最常用、最简单的主线知识点给贯通,给它学会了,然后再逐渐细化,在实践中慢慢探索这些分支,这样学习起来才是比较容易的,所以大家在看手册学习时,有一些感觉非常偏又非常难的知识点,可以先不必深究,先把主线任务学习好,其他的可以之后再研究。

好,那我们来看一下功能参数:

可配置 8 位/ 16 位数据帧、高位先行/低位先行

SPI 最常用的配置就是 8 位数据帧,高位先行。那 16 位数据帧是什么意思呢,我们可以看一下 SPI 波形,可以看到,每次交换数据,都是 8 个 bit,这就是 8 位数据帧,8 位数据帧是最常见的,我们通信一般都是以字节为单位,一个字节就是 8 位。那 STM32 还可以设计成 16 位的数据帧,就是我们可以在 DR 写入一个 uint16_t 类型的数据,这样它一次性就可以发 16 位,也就是两个字节。在这个波形上看,16 位数据帧的波形,应该是和发两次 8 位数据是一样的,我们一般用 8 位即可,16 位的用的很少。然后还有就是高位先行和低位先行,在 SPI 里,基本上都是高位先行的,比如这里的,我想发 0x34,把它转为二进制,就是 0011 0100,左边是数据高位,右边是数据低位,在产生波形时,数据是由高位到低位依次出现的,这就是高位先行。我们 SPI 和 I2C,都采用了高位先行的策略,与之不同的是,串口,串口是低位先行的,我们看一下波形。比如发送 0x55 的波形,0x55 转为二进制是 0101 0101,左边是数据高位,右边是数据低位,在产生波形时,数据由低位到高位依次出现,所以这里的波形,从前到后是 1010 1010,在看它对应的数据是,要从右往左看,0101 0101 对应 0x55,这样才对。低位先行,要反过来看,这个注意一下。

时钟频率,就是 SCK 波形的频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)

一个 SCK 时钟,交换一个 bit,所以时钟频率一般体现的是传输速度,单位是 Hz 或者 bit/s。那这里的时钟频率是 fPCLK 除一个分频系数,分频系数可以配置为 2 或 4 或 8、16、32、64、128、256。所以可以看出,SPI 的时钟,其实就是由 PCLK 分频得来的,PCLK(Perpheral Clock)就是外设时钟,APB2 的 PCLK 就是 72 MHz,APB1 的 PCLK 是 36 MHz,比如我们的 SPI1,是 APB2 的外设,PCLK = 72 MHz。那它的 SPI 时钟频率,最大就是只进行二分频,72 M/2 = 36 MHz,像我们之前 I2C 的频率,最大就只有 400 KHz,所以这里 SPI 的最大速度,比 I2C 快了 90 倍。 然后这里频率有些注意事项。

这个频率数值并不是任意指定的,它只能是 PCLK 执行这些分频后的数值,就只有这 8 个选项,最低频率是 PCLK 的 256 分频。SPI1 和 SPI2 挂载的总线是不一样的,SPI1 挂载在 APB2,PCLK 是 72M,SPI2 等等挂载在 APB1,PCLK 是 36M。所以,同样的配置,SPI1 的时钟频率要比 SPI2 的大一倍,这个注意一下。

那这就是时钟的配置。

支持多主机模型、主或从操作

这个 SPI 多主机模型,只作了解即可。和之前 I2C 一样,手册里 也有大量篇幅介绍了多主机或者从机模式,写的还是比较复杂的,不过这些我们用的都很少,我们只学习 SPI 做主机的情况就行了。

可精简为半双工/单工通信

这些也是为了照顾特殊情况设定的,就是我们正常的 SPI 是两根数据线:一根 MOSI 用于主机发送,一根 MISO 用于主机接收,这是全双工。

半双工就是,如果我们去掉一根数据线,只在其中一根线分时进行发送或接收,这就是半双工的通信,节省一根通信线。单工就是如果我们直接去掉接收的数据线,在发送线进行只发的数据传输,那就是单工通信,只发模式。如果直接去掉发送的数据线,在接收线进行只收的数据传输,那就是单工通信,只收模式。

这些是 STM32 对 SPI 进行的一些改动,可以为特殊情况提供硬件支持。不过既然我们选择了 SPI,正常情况下,就应该发挥它全双工的优势,对吧,所以这些精简模式我们一般不用,大家了解即可。

支持 DMA

这个是让 DMA 来帮我们自动搬运数据的,如果我们需要快速传输大量的数据,那使用 DMA 就比我们手动操作更高效,当然少量数据的话,就没必要了。

兼容 I2S 协议

这个 I2S 是一种音频传输协议,它和 I2C 的区别还是挺大的,不要搞混了。I2S 和 I2C,除了名字差不多,都是飞利浦公司提出的外,其他地方,基本上是完全不一样的。 I2S 是一种数字音频信号传输的专用协议,大概的应用场景,我简单介绍一下,大家了解即可。比如我们的主控芯片,芯片里面存了一首音乐的数据,这个数据其实就是一个点一个点的电压数值,是数字信号。如果我们要把这个音乐给播放出来,就得外挂一个音频解码器,这个音频解码器,实际上就是 DAC,数模转换器,它负责把数字信号转换成模拟信号,然后输出到扬声器,把音乐播放出来,那我们把数字信号发送到 DAC 的这条线路,数据怎么发,每位数据的定义是什么,时序该怎么产生,传输速度是多快,这些配置都可以制定一个标准,这个标准就是数字音频传输协议。那 I2S,就是其中一种,这就是 I2S 应用的领域,主要用来传输数字音频信号,那因为这个 I2S,和 SPI 还是有些共同的特性,所以 STM32 就把 SPI 和 I2S 做到了一起,有些电路可以共用,最大化利用资源。然后手册里,SPI 这一章,也是有一半的内容,都在介绍 I2S,这个大家知道 I2S 是什么东西就行了,感兴趣的话可以看看,我们暂时用不到。

好,那这些就是 SPI 外设大概的技术参数了。功能虽然很多,但是后面这些,都只了解即可。

最后看一下我们这个型号的 STM32(STM32F103C8T6)的硬件 SPI 资源:SPI1、SPI2

其中 SPI1 是 APB2 的外设,SPI2 是 APB1 的外设。这个,开启时钟的时候需要注意一下,另外如果要计算 SCK 时钟频率的话,也要注意一下这个问题,剩下其他的操作,基本上没有区别。

那简介我们就看到这里。

接下来,看一下 SPI 的框图。

那先整体看一下这个图。 我们可以大致把它分为两部分,左上角这一部分,就是数据寄存器和移位寄存器打配合的过程,这个和串口、I2C 那里的设计思路都是异曲同工的,主要是为了实现连续的数据流,一个个数据前仆后继的一个效果。然后剩下右下角这一部分,就是一些控制逻辑了,寄存器的哪些位,控制哪些部分,会产生哪些效果,这个可以通过手册的寄存器描述来得知,至于执行细节,这里也没详细画,我们就知道功能就行了。

那我们接着来详细看一下每部分的功能。

首先左上角,核心部分,就是这个移位寄存器,右边的数据低位,一位一位的,从 MOSI 移出去;然后 MISO 的数据,一位一位的,移入到左边的数据高位,显然移位寄存器应该是一个右移的状态,所以目前图上表示的是低位先行的配置。对应右下角有一个 LSBFIRST 控制位,这一位可以控制是低位先行还是高位先行,手册里,寄存器描述可以查一下(LSBFIRST 帧格式,给 0,先发送 MSB,MSB 就是高位的意思;给 1,先发送 LSB,LSB 就是低位的意思)。目前的状态 LSBFIRST 应该是 1,低位先行;如果 LSBFIRST 给 0,高位先行的话,这个图还要变动一下,就是移位寄存器变为左移,输出从左边移出去,输入从右边移进来,这样才符合逻辑,对吧。

然后继续看左边这一块,画了个方框,里面把 MOSI 和 MISO 做了个交叉,这一块主要是用来进行主从模式引脚变换的。我们这个 SPI 外设,可以做主机,也可以做从机。做主机时,这个交叉就不用,MOSI 为 MO,主机输出;MISO 为 MI,主机输入,这是主机的情况。如果我们 STM32 作为从机的话,MOSI 为 SI,从机输入,这时它就要走交叉的这一路,输入到移位寄存器;同理,MISO,为 SO,从机输出,这时,输出的数据也走交叉的这一路,输出到 MISO。当然这里,如果这样理解没错的话,内部向上的这个箭头可能是画错方向了,应该是往下指的,这样才符合逻辑,那这就是这个交叉的作用。简而言之,就是主机和从机的输入输出模式不同,如果要切换主机和从机的话,线路就需要交叉一下;当然如果我们始终做主机的话,那这个交叉就不用看了。

那这两块电路,是不是就和我们前面移位示意图中介绍的一样。移位示意图中主机里,就是这样的结构,画的是高位先行,所以高位输出到 MOSI,MISO 输入到低位。然后还可以看到,MOSI,主机是输出,从机是输入;MISO,主机是输入,从机是输出,如果你主机要变为从机,那显然,MOSI 和 MISO 要交叉一下。这就是这一部分内容。

接下来,上下两个缓冲区,就还是我们熟悉的设计。这两个缓冲区,实际上就是数据寄存器 DR,下面发送缓冲区,就是发送数据寄存器 TDR,上面接收缓冲区,就是接收数据寄存器 RDR,和串口那里一样,TDR 和 RDR 占用同一个地址,统一叫作 DR。写入 DR 时,数据从地址和数据总线写入到 TDR;读取 DR 时,数据从接收缓冲区(RDR)读出。数据寄存器和移位寄存器打配合,可以实现连续的数据流。 具体流程就是:比如我们需要连续发送一批数据。第一个数据,写入到 TDR,当移位寄存器没有数据移位时,TDR 的数据会立刻转入移位寄存器,开始移位,这个转入时刻,会置状态寄存器的 TXE 为 1,表示发送寄存器空。当我们检查 TXE 置 1 后,紧跟着,下一个数据,就可以提前写入到 TDR 里候着了,一旦上个数据发完,下一个数据就可以立刻跟进,实现不间断的连续传输。然后移位寄存器这里,一旦有数据过来了,它就会自动产生时钟,将数据移出去,在移出的过程中,MISO 的数据也会移入,一旦数据移出完成,数据移入是不是也完成了,这时,移入的数据,就会整体的从移位寄存器转入到接收缓冲区 RDR,这个时刻,会置状态寄存器的 RXNE 为 1,表示接收寄存器非空,当我们检查 RXNE 置 1 后,就要尽快把数据从 RDR 读出来,在下一个数据到来之前,读出 RDR,就可以实现连续接收。否则,如果下一个数据已经收到了,上一个数据还没从 RDR 读出来,那 RDR 的数据就会被覆盖,就不能实现连续的数据流了。这就是移位寄存器配合数据寄存器实现连续数据流的过程。 简而言之,就是发送数据先写入 TDR,再转到移位寄存器发送,发送的同时,接收数据,接收到的数据,转到 RDR,我们再从 RDR 读取数据。数据寄存器和移位寄存器配合,可以实现无延迟的连续传输,这就是这一块的设计思路。是不是和之前串口、I2C 的都差不多的意思啊。当然,这三者也是有一些区别的,比如 SPI 是全双工,发送和接收同步进行,所以它的数据寄存器,发送和接收是分离的,而移位寄存器,发送和接收可以共用。然后看一下前面 I2C 的框图,因为 I2C 是半双工,发送和接收不会同时进行,所以它的数据寄存器和移位寄存器,发送和接收都可以是共用的。最后再看一下串口的框图,串口是全双工,并且发送和接收可以异步进行,所以这就要求它的数据寄存器,发送和接收是分离的,移位寄存器,发送和接收也得是分离的,这就是这三个通信协议,在这一块的设计,设计理念相同,但是执行的细节是不同的。

好,我们回到 SPI 这里,继续来看,这个框图左上角就介绍完了,左上角这一块是比较重要的,它是 SPI 通信的核心部分,体现了发送和接收的执行流程,也是我们写程序的依据,所以需要重点掌握。然后接下来我们看一下右下角这些内容,这就是一些控制逻辑了,我们看一下。

首先是波特率发生器,这个主要就是用来产生 SCK 时钟的。它的内部,主要就是一个分频器,输入时钟是 PCLK,72M 或 36M,经过分频器之后,输出到 SCK 引脚,当然这里生成的时钟肯定是和移位寄存器同步的了,每产生一个周期的时钟,移入移出一个 bit。然后右边,CR1 寄存器的三个位 BR0、BR1、BR2 用来控制分频系数,手册里可以看一下,这里,BR[2:0],是波特率控制,这三位写入下面这些值,可以对 PCLK 时钟执行 2~256 的分频,分频之后,就是 SCK 时钟。所以,这一块,就对应了之前简介里说的时钟频率是 fPCLK 的 2~256 分频,那这就是波特率发生器部分。接着后面这些通信电路和各种寄存器,都是一些黑盒子电路,如果你要具体研究,可以看一下这些位的寄存器描述,我挑几个重点的介绍一下。比如,LSBFIRST,刚才说过,决定高位先行还是地位先行;SPE(SPI Enable)是 SPI 使能,就是 SPI_Cmd 函数配置的位;BR(Baud Rate)配置波特率,就是 SCK 时钟频率;MSTR(Master)配置主从模式,1 是主模式,0 是从模式,我们一般用主模式;CPOL 和 CPHA,这个之前讲过,用来选择 SPI 的 4 种模式。然后,这里 SR 状态寄存器,最后两个,TXE,发送寄存器空;RXNE,接收寄存器非空.这两个比较重要,我们发送接收数据的时候,需要关注这两位。之后 CR2 寄存器,就是一些使能位了,比如中断使能,DMA 使能等,然后剩下的一些位,用的不多,大家可以再自行研究。

那最后,左下角还有个 NSS 引脚。SS 就是从机选择,低电平有效,所以这里前面加了个 N,这个 NSS,和我们想象的从机选择可能不太一样,我们想象的应该是,用来指定某个从机,对吧。但是根据手册里的描述研究了一下,这里的 NSS 设计,可能更偏向于实现简介里说的多主机模型。总的来说,这个 NSS 我们并不会用到,SS 引脚,我们直接使用一个 GPIO 模拟就行,因为 SS 引脚很简单,就置一个高低电平就行了。而且多从机的情况下,SS 还会有多个,这里硬件的 NSS 也完成不了我们想要的功能,那这个 NSS 是如何实现多主机切换的功能呢?简单介绍一下,大家看一看就行,不用掌握。假如有 3 个 STM32 设备,我们需要把这 3 个设备的 NSS 全都连接在一起。首先,这个 NSS 可以配置为输出或者输入,当配置为输出时,可以输出电平告诉别的设备,我现在要变为主机,你们其他设备都给我变为从机,不要过来捣乱;当配置为输入时,可以接收别设备的信号,当有设备是主机,拉低 NSS 后,我就无论如何也变不成主机了,这是它的作用。然后内部电路的设计,当 SPI_CR2 寄存器中的 SSOE = 1 时,NSS 作为输出引脚,并在当前设备变为主设备时,给 NSS 输出低电平,这个输出的低电平,就是告诉其他设备,我现在是主机了;当主机结束后,SSOE 要清 0,NSS 变为输入,这时,输入信号就会跑到通信电路右边的数据选择器,SSM 位决定选择哪一路,当选择上面一路时,是硬件 NSS 模式,也就是说,这时外部如果输入了低电平,那当前的设备就进入不了主模式了,因为 NSS 低电平,肯定是外部已经有设备进入了主模式,它已经提前告诉我它是主模式了,我就不能再跟它抢了;当数据选择器选择下面一路时,是软件管理 NSS 输入,NSS 是 1 还是 0,由这一位 SSI 来决定。这个多主机的模型,举个例子就是,NSS 输入,就是留了个小辫子,一旦我的小辫子被别人揪住了,我就只能乖乖听话,所以我可以把所有人的小辫子接在一起,谁要当主机,就先跳出来,从自己的小辫子输出低电平,揪住其他所有人的小辫子,这时其他所有人都得乖乖听话了。如果这时,其他人也想跳出来做主机呢,它就会发现自己的小辫子被别人揪住了,那它就做不了主机了,只能先乖乖听话,这就是这个 NSS 实现多主机的思路。当然这个设计,使 NSS 作为多从机选择的作用消失了,揪住所有人的小辫子之后,主机发送的数据,就只能是广播发送给所有人的了,如果想实现指定设备通信,可能还需要再加入寻址机制,所以实现起来还是比较复杂的。当然我自己其实也没试过这种玩法,这里是根据看手册的理解,觉得应该是这样玩的,实际使用的话,可能还有很多事情要考虑。不过 SPI最多的情况还是一主多从,或者一主一从,我们掌握一主多从就行,多主机的情况,了解即可。

好,那这个 SPI 框图我们就讲完了,这个图,我们需要重点掌握移位寄存器和数据寄存器这部分。然后,波特率发生器和部分重要的寄存器,也理解一下。最后,NSS 引脚实现多主机,了解即可,我们暂时用不到。

那看完了详细的框图,我们再看一下总结的一个简化结构吧。

SPI 基本结构 这个结构把上面框图里无关的东西都去掉了,这样看起来就更容易理解。大致看一下,其中核心部分当然就是这个数据寄存器和移位寄存器了,这里,发送和接收就直接叫作发送数据寄存器 TDR 和接收数据寄存器 RDR 了,因为觉得这样表示更清晰。之前串口框图里也是这样表示的,但是 SPI 框图这里,它又叫发送缓冲区和接收缓冲区,命名可能不太统一,因为这个手册可能是多个人分工写,最后整合到一起的,所以有时候就发现,手册不同的章节,描述手法和词汇可能都不一样,但是大家要有自己的判断,知道它们其实是一个东西就行。

然后移位寄存器画的是左移,高位移出去,通过 GPIO,到 MOSI。从 MOSI 输出,显然这是 SPI 的主机,对吧,之后移入的数据,从 MISO 进来,通过 GPIO,到移位寄存器的低位,这样循环 8 次,就能实现主机和从机交换一个字节,然后 TDR 和 RDR 的配合,可以实现连续的数据流,这个刚才和以前的章节已经分析过很多次了。另外,TDR 数据,整体转入移位寄存器的时刻,置 TXE 标志位,移位寄存器数据,整体转入 RDR 的时刻,置 RXNE 标志位,TDR、TXE、RDR、RXNE 这几个词再记一下,等会儿会经常提到的,别搞混了。

然后剩下的部分,波特率发生器,产生时钟,输出到 SCK 引脚;数据控制器呢,就看成是一个管理员,它控制着所有电路的运行;最后,开关控制,就是 SPI_Cmd,初始化之后,给个 ENABLE,使能整个外设。另外,这里我并没有画 SS,从机选择引脚,这个引脚,我们还是使用普通的 GPIO 口来模拟即可。在一主多从的模型下,GPIO 模拟的 SS 是最佳选择。

好,那这就是 SPI 的系统框图和简化的结构了。我们在写代码的时候,会用一个结构体来统一配置这些部分,比如高位先行还是低位先行,CPOL 和 CPHA 选择 SPI 模式,主机还是从机,时钟频率是多少,等等等等,这些参数,用一个结构体统一配置,选选参数就行了。所以代码部分,应该是不复杂的。

那初始化部分解决之后,我们就要来看一些运行控制的部分了。如何来产生具体的时序呢,什么时候写 DR,什么时候读 DR呢,这就是我们接下来要学习的知识点。读写 DR,产生时序的流程,我们主要看两个时序图即可。

第一个是主模式全双工连续传输 这个图演示的是借助缓冲区,数据前仆后继,实现连续数据流的过程。但是这个流程,稍微比较复杂,也不太方便封装,所以,在实际过程中,如果对性能没有极致的追求,我们更倾向使用下面这个非连续传输的示意图。

这个非连续传输使用起来更加简单,实际用的话,只需要 4 行代码就能完成任务了。那参考网上别人的代码呢,基本上都是非连续传输的方式,我们本章节也使用非连续传输的代码。非连续传输的好处就是,容易封装,好理解,好用,但是会损失一丢丢性能。连续传输呢,传输更快,但是操作起来相对复杂,那我们来分别具体分析一下。

先看一下主模式全双工连续传输的示意图。首先,第一行是 SCK 时钟线,这里,CPOL = 1,CPHA = 1,示例使用的是 SPI 模式 3,所以 SCK 默认是高电平,然后在第一个下降沿,MOSI 和 MISO 移出数据,之后,上升沿移入数据,依次这样来进行。那下面,第二行是 MOSI 和 MISO 输出的波形,跟随 SCK 时钟变换,数据位依次出现,这里从前到后,依次出现的是 b0、b1,一直到 b7,所以这里示例演示的是低位先行的模式,实际 SPI 高位先行用的多一些。这个知道一下,当然对我们理解时序影响也不大。之后第三行是 TXE,发送寄存器空标志位,波形如图所示,等会再分析。下面继续看,是发送缓冲器,括号,写入 SPI_DR,实际上就是 SPI 基本结构里的 TDR。然后 BSY,BUSY,是由硬件自动设置和清除的,当有数据传输时,BUSY 置 1,那上面这部分,演示的就是输出的流程和现象。

然后下面,是输入的流程和现象。第一个是 MISO/MOSI 的输入数据。之后是 RXNE,接收数据寄存器非空标志位,最后是接收缓冲器,读出 SPI_DR,显然就是 SPI 基本结构里的 RDR了。

好,了解完各个信号的定义了,我们来从左到右依次分析。首先 SS 置低电平,开始时序,这个没画,但是是必须得有的,在刚开始时,TXE 为 1,表示 TDR 空,可以写入数据开始传输。然后,下面指示的第一步就是,软件写入 0xF1 至 SPI_DR,0xF1,就是要发送的第一个数据,之后可以看到,写入之后,TDR 变为 0xF1,同时,TXE 变为 0,表示 TDR 已经有数据了,那此时,TDR 是等侯区,移位寄存器才是真正的发送区,移位寄存器刚开始肯定没有数据,所以在等候区 TDR 里的 F1,就会立刻转入移位寄存器,开始发送,转入瞬间,置 TXE 标志为 1,表示发送寄存器空。然后移位寄存器有数据了,波形就自动开始生成。

当然我感觉这里画的数据波形时机可能有点早,应该是在 TXE 置 1 时刻,b0 的波形才开始产生,在这之前,数据还没有转入移位寄存器呢,所以感觉 b0 出现的可能过早了,不过这个也不影响我们理解,大家知道这意思就行。

好,那这样,数据转入移位寄存器之后,数据 F1 的波形就开始产生了,在移位产生 F1 波形的同时,等候区 TDR 是空的,为了移位完成时,下一个数据能不间断地跟随,我们就要提早把下一个数据写入到 TDR 里等着了。所以下面指示第二步的操作是写入 F1 之后,软件等待 TXE = 1,一旦 TDR 空了,我们就写入 F2 至 SPI_DR,写入之后,可以看到,TDR 的内容,就变成 F2 了,也就是把下一个数据放到 TDR 里候着。之后的发送流程也是同理,F1 数据波形产生完毕后,F2 转入移位寄存器开始发送,这时 TXE = 1, 我们尽快把下一个数据 F3 放到 TDR 里等着,这就是这里的操作。软件等待 TXE = 1,然后写入 F3 至 DR,写入之后 TDR 变为 F3。最后在这里,如果我们只想发送 3 个数据,F3 转入移位寄存器之后,TXE = 1,我们就不需要继续写入了,TXE 之后一直是 1。注意,在最后一个 TXE = 1 之后,还需要继续等待一段时间,F3 的波形才能完整发送,等波形全部完整发送之后,BUSY 标志由硬件清除,这才表示,波形发送完成了。那这些就是发送的流程。

然后继续看一下下面接收的流程,SPI 是全双工,发送的同时,还有接收。所以可以看到,在第一个字节发送完成后,第一个字节的接收也完成了,接收到的数据 1,是 A1。这时,移位寄存器的数据整体转入 RDR,RDR 随后存储的就是 A1。转入的同时,RXNE 标志位也置 1,表示收到数据了,我们的操作是图中下面写的软件等待 RXNE = 1,= 1 表示收到数据了,然后从 SPI_DR,也就是 RDR,读出数据 A1,这就是第一个接收到的数据。接收之后,软件清除 RXNE 标志位,然后,当下一个数据 2 收到之后,RXNE 重新置 1,我们监测到 RXNE = 1 时,就继续读出 RDR,这是第二个数据 A2。最后,在最后一个字节时序完全产生之后,数据 3 才能收到,所以数据 3,直到此时才能读出来。然后注意,一个字节波形收到后,移位寄存器的数据自动转入 RDR,会覆盖原有的数据,所以,我们读出 RDR 要及时。比如 A1 这个数据,收到之后,最迟,也要在 A2 收到之前把它读走,否则,下一个数据 A2,覆盖 A1,就不能实现连续数据流的接收了。

这就是整个发送和接收的流程。这个连续数据流,对软件的配合要求较高。在每个标志位产生后,你的数据都要及时处理。配合的好,时钟可以连续不间断的产生,每个字节之间,没有任何空隙,传输效率是最高的。如果你对传输效率有非常高的要求,那需要好好研究这个连续数据流传输。

但是我们入门的话,可以先不用这个,因为这个操作比较复杂,而且数据的位置交叉比较多。比如我们发送数据 1,按理说,交换字节,发送了,我们就想看一下接收的是什么,但是在接收数据 1 之前,我们就要把发送的数据 2 写入到 TDR 了。所以它的流程并不是我们想象的发送数据 1、接收数据 1、发送数据 2、接收数据 2,这样依次交换,而是发送数据 1、发送数据 2、之后接收数据 1,然后再发送数据 3、接收数据 2、发送数据 4、接收数据 3 这个交换的流程是交错的,对我们程序设计,不太友好。总之,如果你对效率要求很高,就研究一下这个;否则的话,我们更推荐下面整个,非连续传输。

第二个是非连续传输 非连续传输,对于程序设计非常友好,只需要 4 行代码,就可以完成。那它是怎么执行的呢?我们来看一下,这个就是非连续传输发送的示意图,下面这里只有发送的一些波形,接收部分的波形没画出来,但是我们也可以想象的到接收是什么样子的,等会儿我也会给大家指示一下接收的波形。那我们看一下,这个非连续传输和连续传输有什么区别呢?首先,这个配置还是 SPI 模式 3,SCK 默认高电平;我们想发送数据时,如果检测到 TXE = 1 了,TDR 为空,就软件写入 0xF1 至 SPI_DR,这时 TDR 的值变为 F1,TXE 变为 0;目前移位寄存器也是空,所以这个 F1 会立刻转入移位寄存器开始发送,波形产生,并且 TXE 置回 1,表示你可以把下一个数据放在 TDR 里候着了。但是,现在区别就来了,在连续传输中,一旦 TXE = 1 了,我们就会把下一个数据写到 TDR 里候着,这样是为了连续传输,数据衔接更紧密,但是刚才说了,这样的话,流程就比较混乱,程序写起来比较复杂。所以在非连续传输这里,TXE = 1 了,我们不着急把下一个数据写进去,而是一直等待,等第一个字节时序结束;时序结束了,是不是意味着接收第一个字节也完成了,这时接收的 RXNE 会置 1,我们等待 RXNE 置 1 后,先把第一个接收到的数据读出来,之后,再写入下一个字节数据,也就是下面的软件等待 TXE = 1,但是 较晚写入 0xF2 至 SPI_DR;较晚写入 TDR 后,数据 2 开始发送,我们还是不着急写数据 3,等到了时序结束时,先把接收的数据 2 收着,再继续写入数据 3,数据 3 时序结束后,最后再接收数据 3 置换回来的数据。

你看,按照这个流程的话,我们的整体步骤就是:第 1 步,等待 TXE 为 1;第 2 步,写入发送的数据至 TDR;第 3 步,等待 RXNE 为 1;第 4 步,读取 RDR 接收的数据。之后交换第二个字节,重复这 4 步;之后交换第三个字节,也是同理。那这样,我们就可以把这 4 步封装到一个函数,调用一次,交换一个字节,这样程序逻辑是不是就非常简单了,和之前软件 SPI 的流程基本上是一样的,我们只需要稍作修改,就可以把软件 SPI 改成硬件 SPI,这个我们下一小节再来演示。这就是非连续传输的流程。

那非连续传输,缺点就是:在 TXE 置 1 时没有及时把下一个数据写入 TDR 候着,所以等到第一个字节时序完成后,第二个字节还没有送过来,那这个数据传输,就会在这里等着,所以这里时钟和数据的时序,在字节和字节之间会产生间隙,拖慢了整体数据传输的速度。这个间隙,在 SCK 频率低的时候,影响不大,但是在 SCK 频率非常高时,间隙拖后腿的现象,就比较严重了。比如用示波器观察不同 SCK 频率,间隙的影响情况,有 4 个波形,它们的 SCK 分频系数分别是 2、64、128、256。

先看一下最慢的 256 分频,这个 SCK 时钟频率是 72M/256,大概 280K,图示上面是 SCK 信号,这里使用 SPI 模式 0,所以默认低电平;下面是 SS 信号,低电平表示选中从机,这个波形是 SPI 非连续传输,交换 5 个字节的时序,主要看一下 SCK 线,这里连续交换了 5 个字节,但是你几乎看不出字节与字节之间的间隙,对吧。因为这个时钟频率比较慢,间隙时长也不大,所以在这个比较慢的波形看来,间隙对它的影响就可以忽略了。

之后呢,我们加快时钟,看一下波形。下一个图,是 128 分频,SCK 频率,大概 560K。 这时,就可以明显地看出来字节之间的间隙了吧。字节和字节之间,并不是严丝合缝的,这会降低整体的字节传输速度。但是从这个比例上看,这一点点间隙,也可以忽略不计了。

但是之后,我们继续加快时钟。下一个图,64 分频,SCK 频率,大概 1M 多点。 因为频率增大,时间尺度缩小,这样看来,间隙就更加明显了。然后进一步加快时钟频率呢,64 分频后面,还有 32、16、8、4、2 分频,SCK 频率会越来越大,我们直接看一下最快的 2 分频,最后一张图。 这个 SCK 时钟频率,是 72M/2 = 36M,频率非常快了,已经超过这个示波器的采样频率了,所以每个字节的时钟,已经看不完整了,但是哪里在传输,哪里是间隙,还是可以区分的。这里可以看到,间隙所占的时间比例,已经是数据传输的好几倍了,这时,再忽略间隙,就不合适了。如果你忽略间隙,那计算一下 2 分频的数据传输速度,应该是 256 分频的 128 倍。但你实测一下,它肯定达不到这么高,为什么,因为这个 2 分频虽然干活效率高,但是它每干 1 个时间单位,就要休息好几个时间单位,这怎么能达到所声称的效率呢,是吧。

所以通过看这个波形,我们就清楚了,如果你想在极限频率下,进一步提高数据传输效率,也就是追求最高性能,那最好使用连续传输的操作逻辑;或者,还要进一步采用 DMA 自动转运,这些方法效率都是非常高的。否则的话,我们使用非连续传输即可,4 行代码完成任务,简单易理解。

那有关 SPI 的配置和操作流程,就介绍到这里。最后还是惯例,简单看一下软硬件波形对比。

这里上面是软件波形,下面是硬件波形。这些和 I2C 的软硬件波形对比,其实都是差不多的;首先它们的数据变化趋势,肯定是一样的;采样得到的数据,也是一样的。区别就是,硬件波形数据线的变化是紧贴 SCK 边沿的;而软件波形数据线的变化,在边沿后有一些延迟。实际上我们还可以发现,I2C 所描述的 SCL 低电平期间数据变化,高电平期间数据采样和 SPI 所描述的 SCK 下降沿数据移出,上升沿数据移入,最终波形的表现形式,都是一样的;无论是下降沿变化,还是低电平期间变化,它们都是一个意思,下降沿和低电平期间,都可以作为数据变化的时刻,只是硬件波形,一般会紧贴边沿;软件波形,一般只能在电平期间。当然无论是哪种方式,最终都不会影响数据传输,不过软件波形,如果能贴近边沿,我们还是贴近边沿为好;否则,如果你等太久,比较靠近下一个边沿了,那数据也容易出错,对吧。

好,有关 SPI 外设的内容我就介绍到这里。主要是两部分,前面是框图结构,主要用于初始化配置;后面是时序流程,主要用于控制数据传输。

那最后,我们还是来看一下手册。手册第 23 章,是 SPI 的知识点。这里简介写的是,在大容量产品和互联型产品上,SPI 接口可以配置为支持 SPI 协议或者支持 I2S 音频协议,SPI 接口默认工作在 SPI 方式,可以通过软件把功能从 SPI 模式切换到 I2S 模式。在小容量和中容量产品上,不支持 I2S 音频协议。 我们这个 C8T6 芯片是中容量的产品,所以也没有 I2S。

然后下面是 SPI 和 I2S 的主要特性。大家可以看看。接着是框图,这个我们仔细分析过,下面是一些介绍,结合之前的分析,大家可以再看看。NSS 脚管理大家看不懂的话不必深究,我们使用 GPIO 模拟这个引脚,也不用这个功能。然后时钟相位和时钟极性,这个和我们最开始介绍 SPI 那里是一样的,下面的图演示的就是 SPI 的 4 种模式,CPOL,时钟极性,决定空闲状态 SCK 的默认电平,CPHA,时钟相位,决定第一个边沿开始采样还是第二个边沿开始采样,这个图大家可以对照我们前面介绍的,再看看。然后下面,SPI 的从模式,我们用的不多,暂时不用学习。之后主模式,我们需要用到,这里是主模式的配置步骤,但是它都是寄存器的描述方式,实际配置,我们用库函数的初始化结构体来配置,最终实现,和手册里效果一样,而且库函数更容易理解。数据发送和接收过程,就是数据寄存器和移位寄存器的配合过程,另外,TXE、RXNE 这两个重要的标志位,也得理解清楚,只要这个过程清楚了,这里的文字理解起来就容易多了,这是这一块。接着后面,配置 SPI 为单工通信,这个标题,英文手册里写的意思是配置 SPI 为半双工通信,这里直接写的是单工,可能是翻译错了,那下面就是对 SPI 进行一些裁剪了,把标准的全双工 SPI 变为 1 条时钟线和 1 条双向数据线,这其实就是半双工;或者 1 条时钟线和 1 条单向数据线,这就是单工,所以这些是 SPI 裁剪功能的描述,当然我们用的也非常少了,了解即可。之后,数据发送与接收过程,这里分类非常多,但是我们只需要看主模式,全双工的部分就行,其他的基本用不到,那后面这个图就是我们介绍的了,主模式,全双工模式下,连续传输,如果你追求极限频率,就使用这个流程来写程序。之后后面这些其他杂七杂八的模式都不太需要看的。最后是非连续传输发送的流程,这个我们也介绍过,我们写代码就用这种比较简单的方式来写,这也足够满足大多数情况了。然后下面是 CRC 计算,这个是可以在数据流后面加一个 CRC 校验码,用于确定数据有没有传输出错,这个我们也不用,感兴趣的话看一下。接下来,状态标志位,TXE、RXNE,这两个我们提了很多次了,大家应该也清楚了。然后关闭 SPI,这个了解即可。利用 DMA 的 SPI 通信,也了解即可,如果你追求效率的话,再来研究一下 DMA。最后,错误标志和中断,这些需要用的话也可以看看。

那 SPI 的功能描述,差不多就是这些。之后开始就是 I2S 的描述了,我们不用看。最后再大概看一下寄存器描述,第一个控制寄存器 CR1,就是用来配置电路的,我们初始化结构体,也主要就是用来填充这些位的,高位在前的配置、使能、波特率、主设备、SPI 模式都通过写控制寄存器来进行。然后控制寄存器 CR2 是一些中断使能,也是用于配置电路的。然后下面是状态寄存器 SR,有这么多标志位,重点是 TXE 和 RXNE。接着是数据寄存器 DR,手册里写了,数据寄存器对应两个缓冲区,一个用于写(发送缓冲),就是我们说的 TDR;另一个用于读(接收缓冲),就是我们说的 RDR;写操作将数据写到发送缓冲区,读操作将返回接收缓冲区里的数据,这是数据寄存器。之后,CRC 多项式寄存器,这个我们不用。最后,还是 CRC 的两个寄存器,我们也不用。然后后面是 I2S 的寄存器。最后是寄存器的总表,这个寄存器我们就看完了。

那到这里,本小节也就结束了。我们下一小节,来开始写硬件 SPI 的代码

精彩文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: