一、IIC的工作原理
I2C总线是一种十分流行并且强大的总线,其多用于一个(或多个)主机与单个或多个从设备通讯的场景。图1表明了多种不同的外设可以共享这种只需要两根线便可以连接到处理器的总线,相对于其他接口来说,这也是I2C总线可以提供的最大优势之一。
这篇应用笔记的目标是帮助用户理解I2C总线是如何工作的。
图1展示了一个典型的用于嵌入式系统中的I2C总线,其上挂载了多种从设备。作为I2C主机的从微控制器控制着IO拓展、不同传感器、EEPROM、多个ADC/多个DAC、等等。所有这些设备只需要通过来自主机的两根引脚来控制。
I2C总线使用开漏输出控制器,在同一线路上带有一个输入缓冲器,这样便可以允许在单根数据线上实现双向数据流传输。
开漏输出极允许将总线上的电压拉低(大多数情况下是到地),或释放总线以允许其被上拉电阻拉高。当总线被主机或从机释放,线上的上拉电阻负责将线上电压上拉到电源轨。由于并没有设备可以在总线上输出高电平,这也就意味着总线在通讯中,将不会碰到一个设备输出高,而另一个设备试图输出低所导致的短路问题(电源轨到地)。I2C总线要求处于多主机环境下的单个主机在输出高而读回的实际总线电平为低时(这意味着另一个设备拉低了它)中止通讯,因为另一个设备正在使用总线。采用推挽输出方式的接口就没有这么自由了,这也正是I2C总线的一个优先。
图2展示了位于SDA/SCL线上的主从设备的内部简化结构,其由一个用于读取数据的缓冲器,以及一个用于发送数据的下拉场效应管组成。一个设备只被允许拉低总线(规定为短路到地)或释放总线(对地呈现高阻态)以允许上拉电阻拉升总线电平。当处理I2C设备时,有一个重要的概念需要阐明:没有设备可以保持总线为高。这个特性使得双向通讯得以实现。
正如前面章节所述,开漏输出只能将总线拉低,或者释放总线然后依靠上拉电阻拉高总线。图3展示了总线拉低时的电流流向。当逻辑电路想要发送一个低电平时,其会使能下拉场效应管,场效应管会通过短路到地的方式拉低线路。
当从机或主机想要传输一个逻辑电平高,它只能通过使能场效应管的方式释放总线。这将会使得总线处于浮空状态,同时上拉电阻将会将总线电平拉高到供电轨,此电平被当作高电平看待。图4展示了电流如何流过用于拉高总线的上拉电阻。
I2C总线是一种双向接口,其使用被称为主机的控制器与从设备进行通讯。从机不会主动传输任何数据,除非其被主机寻址。每个处于I2C总线上的设备均有独有的设备地址,以用于与位于同一总线上的其他设备做区分。很多从机需要在启动后进行配置以设置设备行为。这通常在主机访问从机的内部寄存器映射时完成,这些寄存器均有独一无二的寄存器地址。单个设备可以具有一个或多个寄存器,这些寄存器可以用来存储或读写数据。
I2C总线的物理接口由串行时钟线(SCL)和串行数据线(SDA)组成。SCL和SDA均需要通过上拉电阻连接到Vcc。上拉电阻的大小由I2C线路上的等效电容大小决定(想要了解更多,可以参考I2C Pull-up Resistor Calculation这份文档,文档号:SLVA689)。数据传送只能在总线空闲时初始化。如果SDA和SCL在一个STOP标志后均处于高电平状态,这时可以认为总线处在空闲状态。
主机访问从机的大体流程如下所示:
假设一个主机想要向从机发送数据:
发送方主机发送一个START标志并且寻址接收方从机
发送方主机发送数据到接收方从机
发送方主机通过发送STOP标志结束传输
如果主机想要从从机接收/读取数据:
接收方主机发送START标志并寻址发送方从机
接收方主机发送需要读取的寄存器地址到发送方从机
接收方主机从发送方从机接收数据
接收方主机通过发送STOP标志结束通讯
主机可以通过发送START标志初始化与设备的I2C通讯,或者发送STOP标志结束通讯。当SCL处于高电平时,SDA上的下降沿意味着一个START标志,而SDA上的上升沿意味着一个STOP标志。
重复的START标志与通常的START标志作用类似,其用于STOP标志后紧接START标志的情况时,用于代替这两者。它看上去与START标志一致,但是与START标志不同的是,重复的START标志在STOP标志之前出现(也就是总线不处于空闲状态时)。当主机希望开始一次新的通讯,但又不希望发送STOP标志使总线进入空闲状态时这会非常管用,这样可以防止当前主机的总线控制权被其他主机抢夺(当处于多主机环境下)。
数据位伴随着SCL上的每一个时钟脉冲被传输。单个字节由SDA线上的8位数据组成,其可以是设备地址、寄存器地址或者读自/写入设备的数据。数据以大端在前(MSB)的方式传输。在START标志与STOP标志之间可以传输任意数量的数据字节。SDA线上的数据必须在时钟电平为高时保持稳定,因为SCL线为高时,SDA线上的变动将会被当作控制指令(START或STOP)。
数据的每一字节(包括地址字节)后总是伴随着来自接收方的1位ACK位。ACK位使得接收方可以告知发送方当前字节已成功接收,并且可以发送下一字节。
在接收方发送ACK位前,发送方必须释放总线。接收方通过在ACK/NACK时钟周期(第9时钟周期)的低电平相位拉低SDA线来发送一个ACK位,如此一来,SDA线将会在ACK/NACK时钟周期的高电平相位保持为低电平。设置与保持时间必须着重注意。
如果SDA线在ACK/NACK时钟周期保持为高电平,这将会被作为NACK。有好几种状态将会导致NACK的产生:
接收方无法进行接收或发送,因为其正在执行一些实时性功能(real-time function),无法与主机进行通讯。
在发送期间,接收方收到了无法识别的数据或指令。
在发送期间,接收方无法接收更多数据字节(也就是缓冲区满了)。
作为接收方的主机完成了数据读取,因此通过发送一个NACK通知从机。
数据可以写入/读自从机,但是这是通过读写从设备内部的寄存器完成的。
包含信息的寄存器处于从机的内存中,无论这些信息是配置信息还是一些需要回发给主机的采样数据。为了指示从机去执行某一任务,主机必须向这些寄存器内写入信息。
虽然通常来说I2C从机是具有多个寄存器的,但也需要注意并不是所有从机都是这样。对于一个只具有单个寄存器的简易从机来说,可以通过在从机地址后直接发送数据的方式来直接写这个单一的寄存器,而不需要再对寄存器进行寻址。一个通过I2C总线控制的8位I2C开关可以很好的作为单寄存器设备的例子。由于它通过1位来使能/失能一个通道,因此只需要1个寄存器,主机可以在从机地址后直接写入寄存器数据,跳过寄存器编码部分。
要在I2C总线上执行写操作,主机会发送一个START标志以及从机地址到总线上,并且将最后1位(读写位)设为0以表明这是写操作。当从机发送应答位之后,主机便发送希望写入的寄存器地址。从机再一次应答,通知主机从机已准备好。这之后,主机开始发送寄存器数据到从机。当主机发送完所有需要发送的数据(有时只是一个字节),其将会通过发送STOP标志结束通讯。
图8展示了一个写入单个字节到从机寄存器的例子。
从从机读取数据与写入数据类似,但是有一些额外的步骤。为了读取从机,主机必须先指示从机自己想要读取哪个寄存器。这一步通过执行与写操作类似的开始通讯步骤完成,发送读写位为0的设备地址(意味着一次写操作),紧跟着希望读的寄存器的地址。一旦从机应答了此地址,主机将会再一次发送START标志,并发送读写位为1的设备地址(意味着一次读操作)。这时,从机将会应答读请求,同时主机释放总线但是保持到从机的时钟供应。在通讯流程的这一部分,主机将会作为接收方主机,同时从机将会作为发送方从机。
主机将会继续发送时钟脉冲,但是会释放SDA线以便于从机传输数据。在每个字节数据的结尾,主机将会发送一个ACK到从机,让从机知道主机准备好接收更多的数据。一旦主机接收完成期待的字节数量,它将会发送一个NACK,通知从机终止通讯并要求从机释放总线。紧接着主机将会发送一个STOP标志结束通讯。
图9展示了从从机寄存器读取单个字节的例子。
这是对德州仪器的应用文档SLVA704的中文翻译,原文标题为:Understanding the I2C Bus。相比于飞利浦出品的几十页的I2C标准文档,此文档只有8页,但是对于想要了解标准I2C总线并应用的人来说,这个已经够用了。
二、地线工作原理
接地可能是一个如此令人困惑的话题的原因之一是该术语被过度滥用。根据上下文,它可能意味着微妙不同但相关的事物。正是出于这个原因,一些工程师不喜欢这个术语,并创造了像本节标题这样的短语。要了解接地,让我们首先定义返回路径,一旦我们了解了返回路径,那么就很容易理解接地。
图 1. 每个正常工作的电路都是一个闭环,总有办法回到源头
图 1 显示了一个非常简单的电路。正如您所看到的,有电流离开电池流过电阻器,流过 LED,然后返回电池。为了使任何电路发挥作用,它必须是一个闭合电路,必须始终有一种方法可以让电流返回源头。无论电路变得多么复杂,总会有一个或多个走线或一个平面作为电流返回源的返回路径。
在几乎所有电路中,这些返回路径统称为“地”。问题在于“地”一词也用于定义电路的参考点。在大多数情况下,两者是一致的(图 2),一切都很清楚,但情况并非总是如此(图 3)。参考点是必要的,因为没有绝对零电压之类的东西。当您测量电压时,它始终与您设计中的某个参考节点相关,并且不一定必须位于返回路径上。事实上,从理论上讲,电路中的任何节点都可以作为参考节点,但是由于我们稍后会讨论的原因,有些节点比其他节点要好。我相信您已经开始看到这会变得令人困惑,我们使用相同的术语来指代两个不同的概念。
图 3. 参考点和返回路径不一致,在复杂的电路中可能是一个令人困惑的噩梦。
在复杂的电路中,我们可以有许多返回路径,其中一些有时被分组为不同的理由。这意味着什么?毕竟,您可能想知道,几段前我说过所有返回路径最终都必须返回源头,而这里我们可能会出现矛盾。请看图 4,我们将一起解决这个问题。
图 4. 不同地的子电路最终都回源
在图 4 中,您可以观察到至少 3 个不同的地面。有一个模拟地 (AGND)、一个数字地 (DGND) 和一个通用地 (GND) [我想让你知道的第一件事是,我为了教育目的制作了这个原理图,你不会指出返回路径像我在这里做的那样使用厚网到源头。就目前而言,它不是有效的 EAGLE 原理图,我只是使用 EAGLE 来绘制]。请注意,三个不同的接地确实返回到源,所以这是一个有效的电路。但是,如果最终它们都只是要返回源头,为什么要分开它们呢?快速的答案是通过将返回路径分组为三个接地,我们可以将一个电路中的噪声电流与其他电路隔离。例如,流经 AGND 电路的电流仅流经连接到 AGND 的那些组件。通过以这种方式设计电路,电流只会在源头处相互影响。使用我们之前的定义,我们可以看到所有返回路径都返回到源头,只是它们的排列经过精心设计,可以在三个电路之间提供一定的抗噪性。
接地、机箱和信号接地。不同名字的玫瑰
有了我们的新定义,让我们分析一些常用的“理由”,我们会发现它们都以相同的方式工作。它们在应用程序的上下文中得到不同的名称。
地球(我们脚下的土壤,而不是地球)被认为是无限的电子源,并定义了我们家中所有电线的参考点(见图 5)。实际上,这条返回路径是通过将金属棒插入地面并确保我们家中的所有“接地”线路牢固地连接(连接)到它来“连接”的。
图 5. 接地棒,连接到房屋并插入大地。因此,接地。
当设备的金属外壳被定义为电路的参考点时,这种类型的接地就得名了。这就是汽车(参见图 6)、洗衣机或任何其他具有导电外壳的设备的情况。使用外壳的底盘和大地作为参考点的主要原因与安全有关。我们的身体几乎总是处于地球电位(或几乎如此)。想象一下,在您要洗衣服的那一刻,洗衣机内的所有电子设备都连接到机箱(机箱接地),机箱连接到插座的接地插头(接地)。如果洗衣机内的高压线与底盘短路会怎样?图 7 给出了答案。
图 6. 连接到汽车底盘的电池负极端子。定义汽车中所有电子设备的参考节点。
图 7. 当大地和机箱接地连接时,电流的返回路径会避开人体,确保您的安全。
如您所见,如果使用机箱和大地接地,则返回路径可以保证在故障期间避免人体接触洗衣机外壳。同样,如果我们考虑返回路径,您会看到在此示例中,机箱接地和接地从返回路径到交流电源。这样可以避免您的身体和洗衣机外壳之间的电位差,这会导致电流流过您的身体。让我们重复这个场景,如果由于某种原因洗衣机的底盘没有接地会发生什么?图 8 显示了痛苦的结果。
图 8. 接地连接断开,您现在是返回路径的一部分。
在这种情况下,您不是一个快乐的露营者,因为与大地的连接已被切断,交流电流只有一条可行的返回路径,您。在这种情况下,当您接触洗衣机外壳时,您会感到震惊。更糟糕的是,电流通常不足以使断路器跳闸,您可能会长时间受到电击。通过明智地选择参考节点,可以设置返回路径以确保您的安全。您已经意识到将这些节点命名为“接地”会混淆对这些安全措施如何工作的理解。
这是最常见的名称,它基本上是我们 PCB 上电路参考节点的定义。通常它是使用接地层物理实现的,这样在我们的设计中就有一条低阻抗返回到电源的路径(参见图 9)。这一点很重要,否则电路板上的不同“地”可能处于不同的电位(参考节点在各处的值不同),这可能导致电路发生故障或无法工作。
图 9. 看到整个 PCB 布局中的纯红色了吗?那是所有组件的铜平面返回路径(信号地)。
正如我们所了解的,每个电气系统都需要至少一条返回电源的返回路径,因此从这个意义上说,所有电路都需要“接地”。通常,这个“地”也将用作参考节点,可以测量电路中的所有电压。然而,并非所有电路都连接到线路电压(即电池供电的设备),因此它们并不都需要接地“接地”或更准确地说是通过接地的返回路径。同样,非导电外壳中的设备也不需要机箱返回路径以确保安全。我们需要的是能够将这些路径称为其他名称以避免与地面混淆,但这超出了本文的范围。
既然您知道了这些类型的“接地”是什么,那么能够在原理图中识别它们非常重要,这样您的电子设备才能以安全的方式正常工作。您将在下面找到表示信号、机箱和接地的最常用符号。虽然这些是标准符号,但您可能会遇到与它们不同的原理图。如果发生这种情况,请务必验证。这样做将确保您是安全的。
我们希望这篇文章有助于澄清关于“地面”是什么的一些困惑。该术语已加载,并且根据上下文可以引用返回路径、引用节点或两者。请记住,这只是冰山一角,整本书都是关于“理由”以及如何在各种应用程序中实现返回路径的。您可能想查看我们最近举办的网络研讨会:PCB 设计信号完整性简介。
您现在有了理解这些书籍并在电路中做出正确设计决策的基础。通过仔细设计返回路径,您可以最大限度地减少电路不同部分之间的串扰,并确保产品用户的安全
三、关于ADC
ADC简介
STM32F103系列有3个ADC,精度为12位,每个ADC最多有16个外部通道。其中ADC1和ADC2都有16个外部通道,ADC3一般有8个外部通道,各通道的A/D转换可以单次、连续、扫描或间断执行,ADC转换的结果可以左对齐或右对齐储存在16位数据寄存器中。ADC的输入时钟不得超过14MHz,其时钟频率由PCLK2分频产生。
ADC功能框图讲解
学习STM32开发板上的外设时首先要了解其外设的功能框图,如下:
功能框图可以大体分为7部分,下面一一讲解:
电压输入范围
ADC所能测量的电压范围就是VREF- ≤ VIN ≤ VREF+,把 VSSA 和 VREF-接地,把 VREF+和 VDDA 接 3V3,得到ADC 的输入电压范围为:0~3.3V。
输入通道
ADC的信号输入就是通过通道来实现的,信号通过通道输入到单片机中,单片机经过转换后,将模拟信号输出为数字信号。STM32中的ADC有着18个通道,其中外部的16个通道已经在框图中标出,如下:
这16个通道对应着不同的IO口,此外ADC1/2/3 还有内部通道:ADC1 的通道 16 连接到了芯片内部的温度传感器, Vrefint 连接到了通道 17。ADC2 的模拟通道 16 和 17 连接到了内部的 VSS。
外部的16个通道在转换时又分为规则通道和注入通道,其中规则通道最多有16路,注入通道最多有4路(注入通道貌似使用不多),下面简单介绍一下两种通道:
规则通道顾名思义就是,最平常的通道、也是最常用的通道,平时的ADC转换都是用规则通道实现的。
注入通道是相对于规则通道的,注入通道可以在规则通道转换时,强行插入转换,相当于一个“中断通道”吧。当有注入通道需要转换时,规则通道的转换会停止,优先执行注入通道的转换,当注入通道的转换执行完毕后,再回到之前规则通道进行转换。
转换顺序
知道了ADC的转换通道后,如果ADC只使用一个通道来转换,那就很简单,但如果是使用多个通道进行转换就涉及到一个先后顺序了,毕竟规则转换通道只有一个数据寄存器。多个通道的使用顺序分为俩种情况:规则通道的转换顺序和注入通道的转换顺序。
规则通道中的转换顺序由三个寄存器控制:SQR1、SQR2、SQR3,它们都是32位寄存器。SQR寄存器控制着转换通道的数目和转换顺序,只要在对应的寄存器位SQx中写入相应的通道,这个通道就是第x个转换。具体的对应关系如下:
通过SQR1寄存器就能了解其转换顺序在寄存器上的实现了:
和规则通道转换顺序的控制一样,注入通道的转换也是通过注入寄存器来控制,只不过只有一个JSQR寄存器来控制,控制关系如下:
需要注意的是,只有当JL=4的时候,注入通道的转换顺序才会按照JSQ1、JSQ2、JSQ3、JSQ4的顺序执行。当JL<4时,注入通道的转换顺序恰恰相反,也就是执行顺序为:JSQ4、JSQ3、JSQ2、JSQ1。
配置转换顺序的函数如下代码所示:
/**
* @brief Configures for the selected ADC regular channel its corresponding
* rank in the sequencer and its sample time.
* @param ADCx: where x can be 1, 2 or 3 to select the ADC peripheral.
* @param ADC_Channel: the ADC channel to configure.
* This parameter can be one of the following values:
* @arg ADC_Channel_0: ADC Channel0 selected
* @arg ADC_Channel_1: ADC Channel1 selected
* @arg ADC_Channel_2: ADC Channel2 selected
* @arg ADC_Channel_3: ADC Channel3 selected
* @arg ADC_Channel_4: ADC Channel4 selected
* @arg ADC_Channel_5: ADC Channel5 selected
* @arg ADC_Channel_6: ADC Channel6 selected
* @arg ADC_Channel_7: ADC Channel7 selected
* @arg ADC_Channel_8: ADC Channel8 selected
* @arg ADC_Channel_9: ADC Channel9 selected
* @arg ADC_Channel_10: ADC Channel10 selected
* @arg ADC_Channel_11: ADC Channel11 selected
* @arg ADC_Channel_12: ADC Channel12 selected
* @arg ADC_Channel_13: ADC Channel13 selected
* @arg ADC_Channel_14: ADC Channel14 selected
* @arg ADC_Channel_15: ADC Channel15 selected
* @arg ADC_Channel_16: ADC Channel16 selected
* @arg ADC_Channel_17: ADC Channel17 selected
* @param Rank: The rank in the regular group sequencer. This parameter must be between 1 to 16.
* @param ADC_SampleTime: The sample time value to be set for the selected channel.
* This parameter can be one of the following values:
* @arg ADC_SampleTime_1Cycles5: Sample time equal to 1.5 cycles
* @arg ADC_SampleTime_7Cycles5: Sample time equal to 7.5 cycles
* @arg ADC_SampleTime_13Cycles5: Sample time equal to 13.5 cycles
* @arg ADC_SampleTime_28Cycles5: Sample time equal to 28.5 cycles
* @arg ADC_SampleTime_41Cycles5: Sample time equal to 41.5 cycles
* @arg ADC_SampleTime_55Cycles5: Sample time equal to 55.5 cycles
* @arg ADC_SampleTime_71Cycles5: Sample time equal to 71.5 cycles
* @arg ADC_SampleTime_239Cycles5: Sample time equal to 239.5 cycles
* @retval None
*/
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime)
{
函数内容略;
}
触发源
ADC转换的输入、通道、转换顺序都已经说明了,但ADC转换是怎么触发的呢?就像通信协议一样,都要规定一个起始信号才能传输信息,ADC也需要一个触发信号来实行模/数转换。
其一就是通过直接配置寄存器触发,通过配置控制寄存器CR2的ADON位,写1时开始转换,写0时停止转换。在程序运行过程中只要调用库函数,将CR2寄存器的ADON位置1就可以进行转换,比较好理解。
另外,还可以通过内部定时器或者外部IO触发转换,也就是说可以利用内部时钟让ADC进行周期性的转换,也可以利用外部IO使ADC在需要时转换,具体的触发由控制寄存器CR2决定。
ADC_CR2寄存器的详情如下:
转换时间
还有一点,就是转换时间的问题,ADC的每一次信号转换都要时间,这个时间就是转换时间,转换时间由输入时钟和采样周期来决定。
由于ADC在STM32中是挂载在APB2总线上的,所以ADC得时钟是由PCLK2(72MHz)经过分频得到的,分频因子由 RCC 时钟配置寄存器RCC_CFGR 的位 15:14 ADCPRE[1:0]设置,可以是 2/4/6/8 分频,一般配置分频因子为8,即8分频得到ADC的输入时钟频率为9MHz。
采样周期是确立在输入时钟上的,配置采样周期可以确定使用多少个ADC时钟周期来对电压进行采样,采样的周期数可通过 ADC采样时间寄存器 ADC_SMPR1 和 ADC_SMPR2 中的 SMP[2:0]位设置,ADC_SMPR2 控制的是通道 0~9, ADC_SMPR1 控制的是通道 10~17。每个通道可以配置不同的采样周期,但最小的采样周期是1.5个周期,也就是说如果想最快时间采样就设置采样周期为1.5.
转换时间=采样时间+12.5个周期
12.5个周期是固定的,一般我们设置 PCLK2=72M,经过 ADC 预分频器能分频到最大的时钟只能是 12M,采样周期设置为 1.5 个周期,算出最短的转换时间为 1.17us。
数据寄存器
转换完成后的数据就存放在数据寄存器中,但数据的存放也分为规则通道转换数据和注入通道转换数据的。
规则数据寄存器负责存放规则通道转换的数据,通过32位寄存器ADC_DR来存放:
当使用ADC独立模式(也就是只使用一个ADC,可以使用多个通道)时,数据存放在低16位中,当使用ADC多模式时高16位存放ADC2的数据。需要注意的是ADC转换的精度是12位,而寄存器中有16个位来存放数据,所以要规定数据存放是左对齐还是右对齐。
当使用多个通道转换数据时,会产生多个转换数据,然鹅数据寄存器只有一个,多个数据存放在一个寄存器中会覆盖数据导致ADC转换错误,所以我们经常在一个通道转换完成之后就立刻将数据取出来,方便下一个数据存放。一般开启DMA模式将转换的数据,传输在一个数组中,程序对数组读操作就可以得到转换的结果。
注入通道转换的数据寄存器有4个,由于注入通道最多有4个,所以注入通道转换的数据都有固定的存放位置,不会跟规则寄存器那样产生数据覆盖的问题。ADC_JDRx 是 32 位的,低 16 位有效,高 16 位保留,数据同样分为左对齐和右对齐,具体是以哪一种方式存放,由ADC_CR2 的 11 位 ALIGN 设置。
从框图中可以知道数据转换完成之后可以产生中断,有三种情况:
规则通道数据转换完成之后,可以产生一个中断,可以在中断函数中读取规则数据寄存器的值。这也是单通道时读取数据的一种方法。
注入通道数据转换完成之后,可以产生一个中断,并且也可以在中断中读取注入数据寄存器的值,达到读取数据的作用。
当输入的模拟量(电压)不再阈值范围内就会产生看门狗事件,就是用来监视输入的模拟量是否正常。
当然,在转换完成之后也可以产生DMA请求,从而将转换好的数据从数据寄存器中读取到内存中。
电压转换
要知道,转换后的数据是一个12位的二进制数,我们需要把这个二进制数代表的模拟量(电压)用数字表示出来。比如测量的电压范围是0~3.3V,转换后的二进制数是x,因为12位ADC在转换时将电压的范围大小(也就是3.3)分为4096(2^12)份,所以转换后的二进制数x代表的真实电压的计算方法就是:
y=3.3* x / 4096
初始化结构体
每个外设的核心就是其对应的初始化结构体了,ADC的初始化结构体代码如下:
typedef struct
{
uint32_t ADC_Mode; // ADC 工作模式选择
FunctionalState ADC_ScanConvMode; // ADC 扫描(多通道)或者单次(单通道)模式选择
FunctionalState ADC_ContinuousConvMode; // ADC 单次转换或者连续转换选择
uint32_t ADC_ExternalTrigConv; // ADC 转换触发信号选择
uint32_t ADC_DataAlign; // ADC 数据寄存器对齐格式
uint8_t ADC_NbrOfChannel; // ADC 采集通道数
} ADC_InitTypeDef;
通过配置初始化结构体来设置ADC的相关信息。
单通道电压采集
用这个程序来简单熟练一下ADC的单通道电压采集吧,程序使用了ADC1的通道11,对应的IO口是PC^1,因为博主的开发板上PC ^1引脚没有任何复用,使用中断,在中断中读取转换的电压。
头文件
为了提高文件的可移植性,头文件中定义了一些与ADC和中断相关的量,在移植程序的时候只需要修改头文件中的定义即可。
#ifndef __ADC_H
#define __ADC_H
#include "stm32f10x.h"
/* 采用ADC1的通道11 引脚为PC^1 模式必须是模拟输入*/
#define ADC_GPIO_RCC RCC_APB2Periph_GPIOC
#define ADC_GPIO_PORT GPIOC
#define ADC_GPIO_PIN GPIO_Pin_1
#define ADC_GPIO_MODE GPIO_Mode_AIN
/* 配置与中断有关的信息 */
#define ADC_IRQn ADC1_2_IRQn
#define ADC_RCC RCC_APB2Periph_ADC1
/* 配置ADC初始化结构体的宏定义 */
#define ADCx ADC1
#define ADCx_ContinuousConvMode ENABLE //连续转换模式
#define ADCx_DataAlign ADC_DataAlign_Right //转换结果右对齐
#define ADCx_ExternalTrigConv ADC_ExternalTrigConv_None //不使用外部触发转换,采用软件触发
#define ADCx_Mode ADC_Mode_Independent //只使用一个ADC,独立模式
#define ADCx_NbrOfChannel 1 //一个转换通道
#define ADCx_ScanConvMode DISABLE //禁止扫描模式,多通道时使用
/* 通道信息和采样周期 */
#define ADC_Channel ADC_Channel_11
#define ADC_SampleTime ADC_SampleTime_55Cycles5
/* 函数声明 */
void ADC_COnfig(void);
void ADC_NVIC_Config(void);
void ADC_GPIO_Config(void);
void ADCx_Init(void);
#endif /* __ADC_H */
引脚配置函数
首先配置相应的GPIO引脚,毕竟模拟信号是通过GPIO引脚传输到开发板的,注意的是,引脚的模式一定要是模拟输入!
void ADC_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(ADC_GPIO_RCC, ENABLE);
GPIO_InitStruct.GPIO_Pin = ADC_GPIO_PIN ;
GPIO_InitStruct.GPIO_Mode = ADC_GPIO_MODE ;
GPIO_Init(ADC_GPIO_PORT , &GPIO_InitStruct);
}
配置引脚就是老套路:声明结构体变量、开启时钟、写入结构体、初始化GPIO。
NVIC配置函数
因为我们是在转换完成后利用中断,在中断函数中读取数据,所以要首先配置中断函数的优先级,因为程序中只有这一个中断,所以优先级的配置就比较随意。
void ADC_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStruct ;
/* 配置中断优先级分组(设置抢占优先级和子优先级的分配),在函数在misc.c */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1) ;
/* 配置初始化结构体 在misc.h中 */
/* 配置中断源 在stm32f10x.h中 */
NVIC_InitStruct.NVIC_IRQChannel = ADC_IRQn ;
/* 配置抢占优先级 */
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1 ;
/* 配置子优先级 */
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1 ;
/* 使能中断通道 */
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE ;
/* 调用初始化函数 */
NVIC_Init(&NVIC_InitStruct) ;
}
ADC配置函数
ADC的配置函数是ADC的精髓,在这个函数中包含的内容有:ADC的初始化结构体配置、配置了时钟分频、配置了通道转换顺序、打开转换中断、进行校准、软件触发ADC采集等。
函数中都有详细的注释:
void ADC_COnfig(void)
{
ADC_InitTypeDef ADC_InitStruct;
RCC_APB2PeriphClockCmd(ADC_RCC, ENABLE);
/* 配置初始化结构体,详情见头文件 */
ADC_InitStruct.ADC_ContinuousConvMode = ADCx_ContinuousConvMode ;
ADC_InitStruct.ADC_DataAlign = ADCx_DataAlign ;
ADC_InitStruct.ADC_ExternalTrigConv = ADCx_ExternalTrigConv ;
ADC_InitStruct.ADC_Mode = ADCx_Mode ;
ADC_InitStruct.ADC_NbrOfChannel = ADCx_NbrOfChannel ;
ADC_InitStruct.ADC_ScanConvMode = ADCx_ScanConvMode ;
ADC_Init(ADCx, &ADC_InitStruct);
/* 配置ADC时钟为8分频,即9M */
RCC_ADCCLKConfig(RCC_PCLK2_Div8);
/* 配置ADC通道转换顺序和时间 */
ADC_RegularChannelConfig(ADCx, ADC_Channel, 1, ADC_SampleTime );
/* 配置为转换结束后产生中断 在中断中读取信息 */
ADC_ITConfig(ADCx, ADC_IT_EOC,ENABLE);
/* 开启ADC,进行转换 */
ADC_Cmd(ADCx, ENABLE );
/* 重置ADC校准 */
ADC_ResetCalibration(ADCx);
/* 等待初始化完成 */
while(ADC_GetResetCalibrationStatus( ADCx))
/* 开始校准 */
ADC_StartCalibration(ADCx);
/* 等待校准完成 */
while (ADC_GetCalibrationStatus(ADCx));
/* 软件触发ADC转换 */
ADC_SoftwareStartConvCmd(ADCx, ENABLE);
}
中断函数
在中断函数中进行读取数据,将数据存放在变量result中,此处使用关键字extern声明,代表变量result已经在其他文件中定义.
extern uint16_t resurt;
void ADC1_2_IRQHandler(void)
{
/* 判断产生中断请求 */
while(ADC_GetITStatus(ADCx, ADC_IT_EOC) == SET)
resurt=ADC_GetConversionValue(ADCx);
/* 清除中断标志 */
ADC_ClearITPendingBit(ADCx, ADC_IT_EOC);
}
主函数
主函数负责接收转换的值,并将其转换为电压值,然后通过串口打印在计算机上,便于调试。
变量result是主函数中的全局变量,注意最后的结果应该转换为浮点型。
#include "stm32f10x.h"
#include "usart.h"
#include "adc.h"
uint16_t result;
void delay(void)
{
uint16_t k=0xffff;
while(k--);
}
int main(void)
{
float voltage;
/* 串口调试函数 */
DEBUG_USART_Config();
/* 与ADC相关的函数打包在此函数中 */
ADCx_Init();
while(1)
{
/* 强制转换为浮点型 */
voltage = (float) result/4096*3.3;
printf("\n电压值为:%f\n",voltage);
delay();
}
}
四、DMA
连接上面这里把DMA说下
直接存储器访问(Direct Memory Access),简称DMA。DMA是CPU一个用于数据从一个地址空间到另一地址空间“搬运”(拷贝)的组件,数据拷贝过程不需CPU干预,数据拷贝结束则通知CPU处理。因此,大量数据拷贝时,使用DMA可以释放CPU资源。在STM32控制器中,芯片采用Cortex-M3架构,总线结构有了很大的优化,DMA占用另外的总线,并不会与CPU的系统总线发生冲突。也就是说,DMA的使用不会影响CPU的运行速度。
DMA数据拷贝过程,典型的有:
内存—>内存,内存间拷贝
外设—>内存,如uart、spi、i2c等总线接收数据过程
内存—>外设,如uart、spi、i2c等总线发送数据过程
串口有必要使用DMA吗
串口(uart)是一种低速的串行异步通信,适用于低速通信场景,通常使用的波特率小于或等于115200bps。对于小于或者等于115200bps波特率的,而且数据量不大的通信场景,一般没必要使用DMA,或者说使用DMA并未能充分发挥出DMA的作用。
对于数量大,或者波特率提高时,必须使用DMA以释放CPU资源,因为高波特率可能带来CPU资源过度浪费的问题。
举个例子
对于发送,使用循环发送,可能阻塞线程,需要消耗大量CPU资源“搬运”数据,浪费CPU。对于发送,使用中断发送,不会阻塞线程,但需浪费大量中断资源,CPU频繁响应中断。以115200bps波特率,1s传输11520字节,大约69us需响应一次中断,如波特率再提高,将消耗更多CPU资源。
对于接收,如仍采用传统的中断模式接收,同样会因为频繁中断导致消耗大量CPU资源。
因此,在高波特率传输场景下,串口非常有必要使用DMA。