上海市工程建设信息网官方网站,网站名称和备案,企业网站托管代运营,企业网站源码网目录 ADC数模转换器DMA直接存储器存取USART串口9-2 串口发送接受9-3 串口收发HEX数据包 I2C(mpu6050陀螺仪和加速度计)SPI协议10.1 SPI简介W25Q64简介10.3 SPI软件读写W25Q6410.4 SPI硬件读写W25Q64 BKP、RTC11.0 Unix时间戳11.1 读写备份寄存器BKP11.2 RTC实时时钟 十二、PWR1… 目录 ADC数模转换器DMA直接存储器存取USART串口9-2 串口发送接受9-3 串口收发HEX数据包 I2C(mpu6050陀螺仪和加速度计)SPI协议10.1 SPI简介W25Q64简介10.3 SPI软件读写W25Q6410.4 SPI硬件读写W25Q64 BKP、RTC11.0 Unix时间戳11.1 读写备份寄存器BKP11.2 RTC实时时钟 十二、PWR12.1 PWR简介12.2 修改主频12.3 数据收发睡眠模式12.4 停止模式12.5 待机模式 十三、看门狗WDG13.1 WDG简介13.2 窗口看门狗WWDG13.3 实现IWDG13.4 实现WWDG ADC数模转换器
那对于GPIO来说它只能读取引脚的高低电平,要么是高电平要么是低电平只有两个值而使用了ADC之后我们就可以对这个高电平和低电平之间的任意电压进行量化最终用一个变量来表示读取这个变量就可以知道引脚的具体电压到底是多少了。所以ADC其实就是一个电压表把引脚的电压值测出来放在一个变量里这就是ADC的作用。 逐次逼近型这是这个ADC的工作模式。然后12位和1us的转换时间这里就涉及到ADC的两个关键参数了第一个是分辨率一般用多少位来表示12位AD值它的表示范围就是0-2^12-1就是量化结果的范围是0~4095。位数越高量化结果就越精细对应分辨率就越高第二个是转换时间就是转换频率AD转换是需要花一小段时间的这里1us就表示从AD转换开始到产生结果需要花1us的时间对应AD转换的频率就是1MHz这个就是STM32 ADC的最快转换频率。如果你需要转换一个频率非常高的信号那就要考虑一下这个转换频率是不是够用如果你的信号频率比较低那这个最大1MHz的转换频率也完全够用了。 外部信号源就是16个GPIO口在引脚上直接接模拟信号就行了不需要任何额外的电路引脚就直接能测电压。2个内部信号源是内部温度传感器和内部参考电压。温度传感器可以测量CPU的温度比如你电脑可以显示一个CPU温度就可以用ADC读取这个温度传感器来测量内部参考电压是一个1.2V左右的基淮电压这个基准电压是不随外部供电电压变化而变化的所以如果你芯片的供电不是标准的3.3V那测量外部引脚的电压可能就不对这时就可以读取这个基准电压进行校准这样就能得到正确的电压值了。 规则组和注入组两个转换单元这个就是STM32 ADC的增强功能了。普通的AD转换流程是启动一次转换、读一次值然后再启动、再读值这样的流程。但是STM32的ADC就比较高级可以列一个组一次性启动一个组连续转换多个值。并且有两个组一个是用于常规使用的规则组一个是用于突发事件的注入组。 模拟看门狗自动监测输入电压范围这个ADC一般可以用于测量光线强度、温度这些值并且经常会有个需求就是如果光线高于某个阈值、低于某个阈值或者温度高于某个阈值、低于某个阈值时执行一些操作。这个高于某个阈值、低于某个阈值的判断就可以用模拟看门狗来自动执行。模拟看门狗可以监测指定的某些通道当AD值高于它设定的上阈值或者低于下阈值时它就会申请中断你就可以在中断函数里执行相应的操作这样你就不用不断地手动读值再用if进行判断了。
ADC可以将模拟信号转换为数字信号是模拟电路到数字电路的桥梁。那反过来有模拟到数字的桥梁那肯定就有数字到模拟的桥梁。这就是DAC数字模拟转换器使用DAC就可以将数字变量转化为模拟电压。 不过在上一节我们还学到了一个数字到模拟的桥梁PWM。上一节我们使用PWM来控制LED的亮度、电机的速度这就是DAC的功能同时PWM只有完全导通和完全断开两种状态在这两种状态上都没有功率损耗。所以在直流电机调速这种大功率的应用场景使用PWM来等效模拟量是比DAC更好的选择并且PWM电路更加简单更加常用。所以可以看出PWM还是挤占了DAC的很多应用空间。 目前DAC的应用主要是在波形生成这些领域比如信号发生器、音频解码芯片等这些领域PWM还是不好替代的。
接下来我们来了解一下这个逐次逼近型ADC到底是怎么测电压的,我们看一下这个图这就是逐次逼近型ADC的内部结构。了解这个结构对你学习STM32的ADC有很大帮助因为STM32的ADC原理和这个是一样的但是STM32只画了一个框表示ADC并没有描述内部结构所以我们先介绍一下这个结构这样再理解STM32的ADC就会简单一些了。 我们来看一下这个图是ADCO809的内部结构图它是一个独立的8位逐次逼近型ADC芯片。在以前单片性能不太好的时候是通过外挂一个ADC芯片才能进行AD转换这个ADCO809就是一款比较经典的ADC芯片。随着单片机的性能和集成度都有很大的提升很多单片机内部就已经集成了ADC外设。 输入选择部分 首先左边这里INO~IN7是8路输入通道通过通道选择开关选中一路输入到所标点进行转换。 下面这里是地址锁存和译码就是你想选中哪个通道就把通道号放在这三个脚ADD…上然后给一个锁存信号(ALU)上面这里对应的通路开关就可以自动拨好了。这部分就相当于一个可以通过模拟信号的数据选择器。 因为ADC转换是一个很快的过程你给个开始信号过几个us就转换完成了。所以说如果你想转换多路信号那不必设计多个AD转换器只需要一个AD转换器然后加一个多路选择开关想转换哪一路就先拨一下开关选中对应通道然后再开始转换就行了。这就是这个输入通道选择的部分这个ADC0809只有8个输入通道我们STM32内部的ADC是有18个输入通道的所以对应输入电路就是一个18路输入的多路开关
核心结构 那然后输入信号选好了到这里所标红点来怎么才能知道这个电压对应的编码数据是多少呢这就需要我们用逐次逼近的方法来——比较了 首先这是一个电压比较器它可以判断两个输入信号电压的大小关系输出一个高低电平指示谁大谁小。它的两个输入端一个是待测的电压另一个是这里DAC的电压输出端DAC是数模转换器。我们之前说过了给它一个数据它就可以输出数据对应的电压DAC内部是使用加权电阻网络来实现的转换具体可以江科大51单片机教程里的AD/DA那一节。 那现在我们有了一个外部通道输入的未知编码的电压和一个DAC输出的已知编码的电压。它俩同时输入到电压比较器进行大小判断如果DAC输出的电压比较大我就调小DAC数据;如果DAC输出的电压比较小我就增大DAC数据直到DAC输出的电压和外部通道输入的电压近似相等 这样DAC输入的数据就是外部电压的编码数据了这就是DAC的实现原理。这个电压调节的过程就是这个逐次逼近SAR来完成的。
为了最快找到未知电压的编码通常我们会使用二分法进行寻找。比如这里是8位的ADC那编码就是从0~255。第一次比较的时候我们就给DAC输入255的一半进行比较那就是128然后看看谁大谁小如果DAC电压大了第二次比较的时候再就给128的一半64如果还大第三次比较的时候就给32如果这次DAC电压小了那第四次就给32到64中间的值然后继续这样依次进行下去就能最快地找到未知电压的编码。并且这个过程如果你用二进制来表示的话你会发现128、64、32这些数据正好是二进制每一位的位权这个判断过程就相当于是对二进制从高位到低位依次判断是1还是0的过程这就是逐次逼近型名字的来源。**那对于8位的ADC从高位到低位依次判断8次就能找到未知电压的编码了对于12位的ADC就需要依次判断12次**这就是逐次逼近的过程。
那然后AD转换结束后DAC的输入数据就是未知电压的编码通过右边电路进行输出8位就有8根线12位就有12根线。
好到这里相信你对逐次逼近型ADC就已经了解差不多了接下来我们就来看看STM32的逐次逼近型ADC看看STM32的ADC和这个相比有什么更高级的变化,那我们看一下STM32的这个ADC框图。
STM(32逐次逼近型)ADC电路图详解 总图
核心的大概工作流程
注入规则组和规则通道组 比喻解释注入组和规则组 这有什么作用呢举个例子这就像是你去餐厅点菜普通的ADC是你指定一个菜老板给你做然后做好了送给你这里就是你指定一个菜单这个菜单最多可以填16个菜然后你直接递个菜单给老板老板就按照菜单的顺序依次做好一次性给你端上菜这样的话就可以大大提高效率。当然你的菜单也可以只写一个菜这样这个菜单就简化成了普通的模式了。 那对于这个菜单呢,也有两种一种是规则组菜单,可以同时上16个菜但是它有个尴尬的地方。就是这个规则组只有一个数据寄存器就是这个桌子比较小最多只能放一个菜你如果上16个菜那不好意思前15个菜都会被挤掉些你只能得到第16个菜。所以对于规则组转换来说如果使用这个菜单的话最好配合DMA来实现。DMA是一个数据转运小帮手它可以在每上一个菜之后把这个菜挪到其他地方去,防止被覆盖。这个DMA我们下一节就会讲现在先大概了解一下那现在我们就知道了这个规则组虽然可以同时转换16个通道但是数据寄存器只能存一个结果如果不想之前的结果被覆盖那在转换完成之后就要尽快把结果拿走。 接着我们看一下注入组这个组就比较高级了它相当于是餐厅的VIP座位在这个座位上一次性最多可以点4个菜并且这里数据寄存器有4个是可以同时上4个菜的。对于注入组而言就不用担心数据覆盖的问题了这就是规则组和注入组的介绍。 一般情况下我们使用规则组就完全足够了如果要使用规则组的菜单那就再配合DMA转运数据这样就不用担心数据覆盖的问题了。所以接下来就只讲规则组的操作注入组涉及的不多大家可以看手册自行了解。
那我们接着继续看这个模数转换器外围的一些线路 首先左下角这里是触发转换的部分,也就是这里的START信号开始转换。那对于STM32的ADC触发ADC开始转换的信号有两种一种是软件触发就是你在程序中手动调用一条代码就可以启动转换了另一种是硬件触发就是这里的这些触发源。上面这些是注入组的触发源下面这些是规则组的触发源这些触发源主要是来自于定时器有定时器的各个通道还有TRGO定时器主模式的输出这个之前讲定时器的时候也介绍过。定时器可以通向ADC、 DAC这些外设用于触发转换。那因为ADC经常需要过一个固定时间段转换一次。比如每隔1ms转换一次正常的思路就是,用定时器,每隔1ms申请一次中断在中断里手动开始一次转换这样也是可以的。但是频繁进中断对我们的程序是有一定影响的比如你有很多中断都需要频繁进入那肯定会影响主程序的执行并且不同中断之间由于优先级的不同也会导致某些中断不能及时得到响应。如果触发ADC的中断不能及时响应那我们ADC的转换频率就肯定会产生影响了。所以对于这种需要频繁进中断并且在中断里只完成了简单工作的情况一般都会有硬件的支持。 比如这里就可以给TIM3定个1ms的时间并且把TIM3的更新事件选择为TRGO输出然后在ADC这里选择开始触发信号为TIM3的TRGO这样TIM3的更新事件就能通过硬件自动触发ADC转换了。整个过程不需要进中断节省了中断资源这就是这里定时器触发的作用。当然这里还可以选择外部中断引脚来触发转换都可以在程序中配置。这就是触发转化的部分。
然后接着看左上角这里是VREF、VREF-、VDDA和VSSA。上面两个是ADC的参考电压决定了ADC输入电压的范围下面两个是ADC的供电引脚。一般情况下VREF要接VDDAVREF-要接VSSA在我们这个芯片上没有VREF和VREF-的引脚它在内部就已经和VDDA和VSSA接在一起了。VDDA和VSSA是内部模拟部分的电源比如ADC、RC振荡器、锁相环等。在这里VDDA接3.3V, VSSA接GND,所以ADC的输入电压范围就是0~3.3V。 然后继续看 右边这里是ADCCLK是ADC的时钟也就是这里的CLOCK是用于驱动内部逐次比较的时钟。这个ADCCLK是来自ADC预分频器而ADC预分频器是来源于RCC的。
APB2时钟72MHZ然后通过ADC预分频器进行分频得到ADCCLKADCCLK最大是14MHZ所以这个预分频器就有点尴尬。它可以选择2、4、6、8分频如果选择2分频72M/236M超出允许范围了4分频之后是18M也超了所以对于ADC预分频器只能选择6分频结果是12M和8分频结果是9M这两个值。这个在程序里要注意一下 继续看上面这里是DMA请求这个就是用于触发DMA进行数据转运的我们下节再讲。
好有关ADC的这个框图我们就介绍完了。
ADC基本结构 那接下来就来看一下我这里总结的一个ADC基本结构图再来回忆一下。 左边是输入通道16个GPIO口外加两个内部的通道然后进入AD转换器。AD转换器里有两个组一个是规则组一个是注入组规则组最多可以选中16个通道注入组最多可以选择4个通道。然后转换的结果可以存放在AD数据寄存器里其中规则组只有1个数据寄存器注入组有4个。 然后下面这里有触发控制提供了开始转换这个START信号触发控制可以选择软件触发和硬件触发。硬件触发主要是来自于定时器当然也可以选择外部中断的引脚右边这里是来自于RCC的ADC时钟CLOCKADC逐次比较的过程就是由这个时钟推动的。 然后上面可以布置一个模拟看门狗用于监测转换结果的范围如果超出设定的阈值就通过中断输出控制向NVIC申请中断另外规则组和注入组转换完成后会有个EOC信号它会置一个标志位当然也可以通向NVIC。最后右下角这里还有个开关控制在库函数中就是ADC_Cmd函数用于给ADC上电的那这些就是STM32 ADC的内部结构了。
接下来我们再了解一些细节的问题这些就是ADC通道和引脚复用的关系这个对应关系也可以通过引脚定义表看出来。另外由于我们这个芯片没有PC0~PC5所以这些通道也就没有了。 ADC1和ADC2的引脚全都是相同的既然都相同那要ADC2还有啥用呢。这个就要再说一个ADC的高级功能了就是双ADC模式,这个模式比较复杂。这里只简单介绍一下不需要掌握。双ADC模式就是ADC1和ADC2一起工作它俩可以配合组成同步模式、交叉模式等等模式。比如交叉模式ADC1和ADC2交叉地对一个通道进行采样这样就可以进一步提高采样率。
规则组的4种转换模式 接下来我们再来了解一下规则组的4种转换模式分别是单次转换非扫描模式和连续转换扫描模式。那在我们ADC初始化的结构体里会有两个参数一个是选择单次转换还是连续转换的另一个是选择扫描模式还是非扫描模式的这两个参数组合起来就有这4种转换方式。我们来逐一看一下。 第一种单次转换非扫描模式这里我画了一个列表这个表就是规则组里的菜单有16个空位分别是序列1到序列16你可以在这里“点菜”就是写入你要转换的通道在非扫描的模式下这个菜单就只有第一个序列1的位置有效这时菜单同时选中一组的方式就退化为简单地选中一个的方式了。在这里我们可以在序列1的位置指定我们想转换的通道比如通道2写到这个位置。然后我们就可以触发转换ADC就会对这个通道2进行模数转换过一小段时间后转换完成转换结果放在数据寄存器里同时给EOC标志位置1整个转换过程就结束了。我们判断这个EOC标志位如果转换完了 那我们就可以在数据寄存器里读取结果了。如果我们想再启动一次转换那就需要再触发一次转换结束置EOC标志位读结果。如果想换一个通道转换那在转换之前把第一个位置的通道2改成其他通道然后再启动转换这样就行了。这就是单次转换非扫描的转换模式。没有用到这个菜单列表也是比较简单的一种模式 接下来我们看一下连续转换非扫描模式。首先它还是非扫描模式所以菜单列表就只用第一个然后它与上一种单次转换不同的是它在一次转换结束后不会停止而是立刻开始下一轮的转换然后一直持续下去。这样就只需要最开始触发一次之后就可以一直转换了。这个模式的好处就是开始转换之后不需要等待一段时间的因为它直都在转换所以你就不需要手动开始转换了也不用判断是否结束的想要读AD值的时候直接从数据寄存器取就是了。这就是连续转换非扫描的模式 然后继续看单次转换扫描模式。这个模式也是单次转换所以每触发一次转换结束后就会停下来下次转换就得再触发才能开始。然后它是扫描模式这就会用到这个菜单列表了你可以在这个菜单里点菜比如第一个菜是通道2第二个菜是通道5等等等等这里每个位置是通道几可以任意指定并且也是可以重复的然后初始化结构体里还会有个参数就是通道数目。因为这16个位置你可以不用完只用前几个那你就需要再给一个通道数目的参数告诉它我有几个通道。比如这里指定通道数目为7那它就只看前7个位置然后每次触发之后它就依次对这前7个位置进行AD转换转换结果都放在数据寄存器里这里为了防止数据被覆盖就需要用DMA及时将数据挪走。那7个通道转换完成之后产生EOC信号转换结束然后再触发下一次就又开始新一轮的转换这就是单次转换扫描模式的工作流程。 那最后再看一下连续转换扫描模式。它就是在上一个模式的基础上变了一点就是一次转换完成后立刻开始下一次的转换。和上面这里非扫描模式的单次和连续是一个套路这就是连续转换扫描模式。
当然在扫描模式的情况下还可以有一种模式叫间断模式。它的作用是在扫描的过程中每隔几个转换就暂停一次需要再次触发才能继续。这个模式没有列出来要不然模式太多了。大家了解一下就可以了暂时不需要掌握好这些就是STM32 ADC的4种转换模式。
几个小知识点|细节 触发控制 这个表就是规则组的触发源也就是ADC总框图中的ADC。在这个表里有来自定时器的信号还有这个来自引脚或定时器的信号这个具体是引脚还是定时器需要用AFIO重映射来确定最后是软件控制位也就是我们之前说的软件触发。这些触发信号怎么选择可以通过设置右边这个寄存器来完成当然使用库函数的话直接给一个参数就行了这就是触发控制。
数据对齐
转换时间
这个大概讲一下不过转换时间这个参数我们一般不太敏感因为一般AD转换都很快如果不需要非常高速的转换频率那转换时间就可以忽略了。 我们来看一下之前我们说了AD转换是需要一小段时间的就像厨子做菜一样也是需要等一会儿才能上菜的那AD转换的时候都有哪些步骤需要花时间呢AD转换的步骤有4步分别是采样保持量化编码其中采样保持可以放在一起量化编码可以放在一起总共是这两大步。量化编码好理解就是我们之前讲过的ADC逐次比较的过程这个是要花一段时间的一般位数越多花的时间就越长。 那采样保持是干啥的呢这个我们前面这里并没有涉及为什么需要采样保持呢这是因为我们的AD转换就是后面的量化编码是需要一小段时间的如果在这一小段时间里输入的电压还在不断变化那就没法定位输入电压到底在哪了所以在量化编码之前我们需要设置一个采样开关。先打开采样开关收集一下外部的电压比如可以用一个小容量的电容存储一下这个电压存储好了之后断开采样开关再进行后面的AD转换。这样在量化编码的期间电压始终保持不变这样才能精确地定位未知电压的位置这就是采样保持电路。 那采样保持的过程需要闭合采样开关过一段时间再断开这里就会产生一个采样时间。那回到这里我们就得到了第二条STM32 ADC的总转换时间为TCONV采样时间12.5个ADC周期采样时间是采样保持花费的时间这个可以在程序中进行配置采样时间越大越能避兔一些毛刺信号的干扰不过转换时间也会相应延长。12.5个ADC周期是量化编码花费的时间因为是12位的ADC所以需要花费12个周期这里多了半个周期可能是做其他一些东西花的时间。ADC周期就是从RCC分频过来的ADCCLK这个ADCCLK最大是14MHz。 所以下面有个例子这里就是最快的转换时间当ADCCLK14MHz采样时间为1.5个ADC周期TCONV 1.5 12.5 14个ADC周期在14MHz ADCCLK的情况下就 1us这就是转化时间最快1us时间的来源。如果你采样周期再长些它就达不到1us了另外你也可以把ADCCLK的时钟设置超过14MHz这样的话ADC就是在超频了那转换时间可以比1us还短不过这样稳定性就没法保证了。
校准
这个看上去挺复杂但是我们不需要理解这个校准过程是固定的。我们只需要在ADC初始化的最后加几条代码就行了至于怎么计算、怎么校准的我们不需要管。
ADC外围电路设计 对于ADC的外围电路我们应该怎么设计呢 如果你想采集5V10V这些电压的话可以使用这个电压转换电路,但是如果你电压再高一些就不建议使用这个电路了那可能会比较危险高电压采集最好使用一些专用的采集芯片比如隔离放大器等等做好高低电压的隔离保证电路的安全。
手册粗讲
代码实战AD单通道AD多通道 7-1 AD单通道 程序现象在面包板的中间也就是芯片左边接了一个电位器就是滑动变阻器。用这个电位器产生一个0~3.3V连续变化的模拟电压信号。然后接到STM32的PA0口上之后用STM32内部的ADC读取电压数据显示在屏幕上。这里屏幕第一行显示的是AD转换后的原始数据第二行是经过处理后实际的电压值。电位器往左拧AD值减小电压值也减小AD值最小是0对应的电压就是0V反之同理STM32的ADC是12位的所以AD结果最大值是4095也就是2^12-1对应的电压是3.3V。 第一步开启RCC时钟包括ADC和GPIO的时钟另外这里ADCCLK的分频器也需要配置一下 第二步配置GPIO。把需要用的GPIO配置成模拟输入的模式
第三步配置这里的多路开关。把左边的通道接入到右边的规则组列表里。这个过程就是我们之前说的点菜把各个通道的菜列在菜单里 第四步就是配置ADC转换器了。在库函数里是用结构体来配置的可以配置这一大块电路的参数。包括ADC是单次转换还是连续转换、扫描还是非扫描、有几个通道触发源是什么数据对齐是左对齐还是右对齐。 如果你需要模拟看门狗那会有几个函数用来配置阈值和监测通道的 如果你想开启中断那就在中断输出控制里用ITConfig函数开启对应的中断输出然后再在NVIC里配置一下优先级这样就能触发中断了。 不过这一块模拟看门狗和中断我们本节暂时不用如果你需要的话可以自己配置试一下
接下来就是开关控制调用一下ADC_Cmd函数开启ADC,这样ADC就配置完成了就能正常工作了。
当然在开启ADC之后根据手册里的建议我们还可以对ADC进行一下校准这样可以减小误差那在ADC工作的时候
这里有四个函数对应校准的四个步骤第一步调用第一个函数ADC_ResetCalibration复位校准第二步调用第二个函数ADC_GetResetCalibrationStatus等待复位校准完成第三步调用第三个函数ADC_StartCalibration开始校准第四步调用第四个函数ADC_GetCalibrationStatus等待校准完成。 如果想要软件触发转换那会有函数可以触发。如果想读取转换结果那也会有函数可以读取结果这个等会儿介绍库函数的时候就可以看到了。好这些就是我们程序的大概思路了。 首先软件触发转换然后等待转换完成也就是等待EOC标志位置1最后读取ADC数据寄存器就完事了。 7-1 AD单通道 程序现象在这里分别接了光敏电阻、热敏电阻和反射红外模块三个传感器模块。把它们的AO、模拟电压输出端分别接在了A1、A2、A3引脚加上刚才的电位器总共4个输出通道。然后测出来的4个AD数据分别显示在屏幕上 现象这里AD值的末尾会有些抖动这是正常的波动如果你想对这个值进行判断再执行一些操作。比如光线的AD值小于某一阈值就开灯大于某一阈值就关灯那可能会存在这样的情况比如光线逐渐变暗AD值逐渐变小但是由于波动AD值会在判断阈值附近来回跳变这会导致输出产生抖动反复开关灯。
那如何避兔这种情况呢有很多种方法比如可以使用迟滞比较的方法来完成设置两个阈值低于下阈值时开灯这就可以避免输出抖动的问题了。另外如果你觉得数据跳变太厉害还可以采取滤波的方法让AD值平滑一些比如均值滤波就是读10个或20个值取平均值作为滤波的AD值或者还可以裁剪分辨率把数据的尾数去掉。
7-2 AD多通道 如何实现多通道呢 我们首先想到的应该是后面这两种扫描模式连续转换、扫描模式和单次转换、扫描模式但如果想要用扫描模式实现多通道最好要配合DMA来实现来解决数据覆盖的问题。 那你可能会问我们一个通道转换完成之后你启动列表之后它里面每一个单独的通道转换完成之后不会产生任何的标志位也不会触发中断你不知道某一个通道是不是转换完了。它只有在整个列表都转换完成之后才会产生一次EOC标志位才能触发中断而这时前面的数据就已经覆盖丢失了。其次AD转化时很快的如果你不能在几us的时间内把数据转运走那数据就会丢失这对我们程序手动转运数据要求就比较高了. 所以在扫描模式下手动转运数据是比较困难的,不过比较困难也不是说手动转运不可行我们可以使用间断模式在扫描的时候每转换一个通道就暂停一次等我们手动把数据转运走之后10再继续触发继续下一次转换。但是由于单个通道转换完成之后没有标志位。所以启动转换之后只能通过Delay延时的方式延迟足够长的时间才能保证转换完成。这种方式既不能让我们省心也不能提高效率所以我暂时不推荐使用。
我们可以使用上面的这个单次转换、非扫描的模式来实现多通道。只需要在每次触发转换之前手动更改一下列表第一个位置的通道就行了 DMA直接存储器存取 所以存储器到存储器的数据转运我们一般使用软件触发,外设到存储器的数据转运我们一般使用硬件触发。
我们来看一下STM32的存储器映像既然DMA是在存储器之间进行数据转运的那我们就应该要了解一下STM32中都有哪些存储器这些存储器又是被安排到了哪些地址上这就是存储器映像的内容。
在这个表里无论是Flash还是SRAM还是外设寄存器它们都是存储器的一种包括外设寄存器实际上也是存储器。在DMA简介中我们说的是外设到存储器存储器到存储器本质上其实都是存储器之间的数据转运说成外设到存储器只不过是STM32他特别指定了可以转运外设的存储器而已。 DMA框图讲解 左上角这里是Cortex-M3内核里面包含了CPU和内核外设等等剩下的这所有东西你都可以把它看成是存储器所以总共就是CPU和存储器两个东西。Flash是主闪存SRAM是运行内存各个外设都可以看成是寄存器也是一种SRAM存储器。 奇存器是一种特殊的存储器一方面CPU可以对奇存器进行读写就像读写运行内存一样另一方面寄存器的每一位背后都连接了一根导线这些导线可以用于控制外设电路的状态比如置引脚的高低电平、导通和断开开关、切换数据选择器或者多位组合起来当做计数器、数据寄存器等等。所以寄存器是连接软件和硬件的桥梁软件读写寄存器就相当于在控制硬件的执行。 回到这里既然外设就是寄存器寄存器就是存储器那使用DMA进行数据转运就都可以归为一类问题了。就是从某个地址取内容再放到另一个地址去。 我们看图为了高效有条理地访问存储器这里设计了一个总线矩阵总线矩阵的左端是主动单元也就是拥有存储器的访问杈右边这些是被动单元它们的存储器只能被左边的主动单元读写。主动单元这里内核有DCode和系统总线可以访问右边的存储器其中DCode总线是专门访问Flash的系统总线是访问其他东西的另外由于DMA要转运数据所以DMA也必须要有访问的主动权。那主动单元除了内核CPU剩下的就是DMA总线了。这里DMA1有一条DMA总线DMA2也有一条DMA总线下面这还有一条DMA总线这是以太网外设自己私有的DMA这个可以不用管的。 在DMA1和DMA2里面可以看到DMA1有7个通道DMA2有5个通道各个通道可以分别设置它们转运数据的源地址和目的地址这样它们就可以各自独立地工作了。 接着下面这里有个仲裁器这个是因为虽然多个通道可以独立转运数据但是最终DMA总线只有一条所以所有的通道都只能分时复用这一条DMA总线。如果产生了冲突那就会由仲裁器根据通道的优先级来决定谁来使用。另外在总线矩阵这里也会有个仲裁器如果DMA和CPU都要访问同一个目标那么DMA就会暂停CPU的访问以防止冲突。不过总线仲裁器仍然会保证CPU得到一半的总线带宽使CPU也能正常的工作。 下面这里是AHB从设备也就是DMA自身的寄存器因为DMA作为一个外设它自己也会有相应的配置寄存器这里连接在了总线右边的AHB总线上所以DMA即是总线矩阵的主动单元可以读写各种存储器也是AHB总线上的被动单元。CPU通过这一条线路就可以对DMA进行配置了。 接着继续看这里是DMA请求请求就是触发的意思这条线路右边的触发源是各个外设所以这个DMA请求就是DMA的硬件触发源。比如ADC转换完成、串口接收到数据需要触发DMA转运数据的时候就会通过这条线路向DMA发出硬件触发信号之后DMA就可以执行数据转运的工作了。这就是DMA请求的作用
到这里有关DMA的结构就讲的差不多了其中包括用于访问各个存储器的DMA总线内部的多个通道可以进行独立的数据转运仲裁器用于调度各个通道防止产生冲突AHB从设备用于配置DMA参数DMA请求用于硬件触发DMA的数据转运这就是这个DMA的各个部分和作用。 注意一下就是这里的Flash它是ROM只读存储器的一种如果通过总线直接访问的话无论是CPU还是DMA都是只读的只能读取数据而不能写入如果你DMA的目的地址填了Flash的区域那转运时就会出错。当然Flash也不是绝对的不可写入我们可以配置这个Flash接口控制器对Flash进行写入这个流程就比较麻烦了要先对Flash按页进行擦除再写入数据。总之就是CPU或者DMA直接访问Flash的话是只可以读而不可以写的然后SRAM是运行内存可以任意读写没有问题外设寄存器的话得看参考手册里面的描述。
DMA基本结构 刚才这个框图只是一个笼统的结构图对于DMA内部的执行细节它还是没体现出来所以我们再来分析一下这个图看看DMA具体是怎么工作的。 这就是外设站点和存储器站点各自的3个参数了。 在STM32手册里所说的存储器一般是特指Flash和SRAM不包含外设寄存器。外设寄存器他一般直接称作外设所以就是外设到存储器存储器到存储器这样来描述。虽然我们刚才说了寄存器也是存储器的一种但是STM32还是使用了外设和存储器来作为区分这个注意一下描述方法的不同。那在这里可以看到 这就是外设站点和存储器站点各自的3个参数了。
传输计数器和自动重装器 触发控制部分
然后最后就是开关控制了也就是DMA_Cmd函数.当给DMA使能后DMA就准备就绪可以进行转运了。
基于DMA基本结构的一些问题 问题1那如何进行存储器到存储器的数据转运方向反过来可以吗 如果要进行存储器到存储器的数据转运。那我们就需要把其中一个存储器的地址放在外设的这个站点这样就能进行存储器到存储器的转运了。只要你在外设起始地址里写Flash或者SRAM的地址那它就会去Flash或SRAM找数据。这个站点虽然叫外设寄存器但是它就只是个名字而已。甚至你可以在外设站点写存储器的地址存储器站点写外设的地址然后方向参数给反过来这样也是可以的只是ST公司给它起了这样的名字而已。你也可以把它叫做站点A、站点B,从A到B或者从B到A转运数据。
问题2在DMA中软件触发的执行逻辑和外部中断、ADC的软件触发有什么区别 这个软件触发并不是调用某个函数一次触发一次它这个软件触发的执行逻辑是以最快的速度连续不断地触发DMA争取早日把传输计数器清零完成这一轮的转换。所以这里的软件触发和我们之前外部中断和ADC的软件触发可能不太一样你可以把它理解成连续触发那这个软件触发和自动重装器循环模式不能同时用。因为软件触发就是想把传输计数器清零循环模式是清零后自动重装如果同时用的话那DMA就停不下来了这就是软件触发。
问题3DMA的转运条件 DMA进行转运有几个条件第一就是开关控制DMA_Cmd必须使能第二就是传输计数器必须大于0第三就是触发源必须有触发信号。触发一次转运一次传输计数器自减一次当传输计数器等于0且没有自动重装时。这时无论是否触发DMA都不会再进行转运了此时就需要DMA_Cmd给DISABLE关闭DMA,再为传输计数器写入一个大于0的数,再DMA_Cmd给ENABLE开启DMADMA才能继续工作。 注意一下写传输计数器时必须要先关闭DMA再进行不能在DMA开启时写传输计数器这是手册里的规定。
几个小知识点|细节 DMA请求 数据宽度与对齐 细节讲解 DMA数据转运的两个站点都有一个数据宽度的参数如果数据宽度都一样那就是正常的一个个转运如果数据宽度不一样那会怎么处理呢 这个表就是来说明问题的 总之一这个表的意思就是如果你把小的数据转到大的里面去,高位就会补0如果把大的数据转到小的里面去高位就会舍弃掉如果数据宽度一样那就没事。
那最后我们再来看两个例子看看在这些实际的任务下DMA是如何工作的。这两个例子和程序例子对应的。
数据转运DMA
这个例子的任务是将SRAM里的数组DataA转运到另一个数组DataB中我们看一下这种情况下这个基本结构里的各个参数该如何配置。 首先是外设站点和存储器站点的起始地址、数据宽度、地址是否自增这三个参数。那在这个任务里外设地址显然应该填DataA数组的首地址存储器地址给DataB数组的首地址然后数据宽度两个数组的类型都是uint8_t所以数据宽度都是按8位的字节传输。之后地址是否自增在中间可以看到我们想要的效果是DataA[]转到DataB[]DataA[1]转到DataB[1]等等。所以转运完DataA[0]和DataB[0]之后两个站点的地址都应该自增都移动到下一个数据的位置继续转运DataA[1]和DataB[1]这样来进行。 之后这里的方向参数那显然就是外设站点转运到存储器站点了当然如果你想把DataB的数据转运到DataA那可以把方向参数换过来这样就是方向转运了。 然后是传输计数器和是否要自动重装在这里显然要转运7次所以传输计数器给7自动重装暂时不需要之后触发选择部分这里我们要使用软件触发。因为这是存储器到存储器的数据转运是不需要等待硬件时机的尽快转运完成就行了。 那最后调用DMA_Cmd给DMA使能这样数据就会从DataA转运到DataB了。转运7次之后传输计数器自减到0DMA停止转运完成。这里的数据转运是一种复制转运转运完成后DataA的数据并不会消失这个过程相当于是把DataA的数据复制到了DataB的位置。
ADC扫描模式DMA 讲解细节精彩 左边是ADC扫描模式的执行流程在这里有7个通道触发一次后7个通道依次进行AD转换然后转换结果都放到ADC_DR数据寄存器里面。那我们要做的就是在每个单独的通道转换完成后进行一个DMA数据转运并且目的地址进行自增这样数据就不会被覆盖了。所以在这里DMA的配置就是外设地址写入ADC_DR这个寄存器的地址存储器的地址可以在SRAM中定义一个数组ADValue然后把ADValue的地址当做存储器的地址。 之后数据宽度因为ADC_DR和SRAM数组我们要的都是uint16_t的数据所以数据宽度都是16位的半字传输。 接着判断地址是否自增那从这个图里显然是外设地址不自增存储器地址自增传输方向是外设站点到存储器站点传输计数器这里通道有7个所以计数7次计数器是否自动重装这里可以看ADC的配置ADC如果是单次扫描那DMA的传输计数器可以不自动重装转换一轮就停止如果ADC是连续扫描那DMA就可以使用自动重装在ADC启动下一轮转换的时候DMA也启动下一轮的转运ADC和DMA同步工作。 最后是触发选择这里ADC_DR的值是在ADC单个通道转换完成后才会有效所以DMA转运的时机需要和ADC单个通道转换完成同步所以DMA的触发要选择ADC的硬件触发。 最后硬件触发这里要说明一下我们上一节说了ADC扫描模式在每个单独的通道转换完成后没有任何标志位也不会触发中断。所以我们程序不太好判断某一个通道转换完成的时机是什么时候。但是根据UP主的研究虽然单个通道转换完成后不产生任何标志位和中断但是它应该会产生DMA请求去触发DMA转运这部分内容手册里并没有详细描述根据我实际实验单个通道的DMA请求肯定是有的。 这些就是ADC扫描模式和DMA配合使用的流程。一般来说DMA最常见的用途就是配合ADC的扫描模式因为ADC扫描模式有个数据覆盖的特征这个缺陷使ADC和DMA成为了最常见的伙伴。
手册
代码实战 DMA数据转运DMAAD多通道 验证存储器映像的内容
8-1 DMA数据转运 也就是把一个数组里面的数据复制到另一个数组里
这就是我们第一个代码的任务定义一下DMA转运的源端数组和目的数组初始化DMA然后让DMA把这里DataA的数据转运到DataB里面去。
初始化第一步RCC开启DMA的时钟 注意这里开启DMA时钟的时候根据型号不同开启时钟参数也不同 第二步就可以直接调用DMA_Init初始化这里的各个参数了,包括外设和存储器站点的起始地址、数据宽度、地址是否自增方向、传输计数器、是否需要自动重装、选择触发源、通道优先级那这所有的参数通过一个结构体就可以配置好了 例
/* Initialize the DMA Channel1 according to the DMA_InitStructure
members */
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr 0x40005400;
DMA_InitStructure.DMA_MemoryBaseAddr 0x20000100;
DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize 256;
DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority DMA_Priority_Medium;
DMA_InitStructure.DMA_M2M DMA_M2M_Disable;
DMA_Init(DMA_Channel1, DMA_InitStructure); 之后就可以进行开关控制DMA_Cmd给指定的通道使能就完成了。那在这里如果你选择的是硬件触发不要忘了在对应的外设调用一下XXX_DMACmd开启一下触发信号的输出如果你需要DMA的中断那就调用DMA_ITConfig开启中断输出再在NVIC里配置相应的中断通道然后写中断函数就行了
最后在运行的过程中如果转运完成传输计数器清0了。这时想再给传输计数器赋值的话就DMA失能、写传输计数器、DMA使能这样就行了。 MyDMA部分 mian.c: 8-2 DMAAD多通道 用ADC的扫描模式来实现多通道采集然后使用DMA来进行数据转运 ADDMA部分
main.c部分
模块篇 按键 旋转编码器 31:05开始介绍了编码器 舵机
如果单独供电的话,供电的负极要和STM32共地然后正极接在5V供电引脚上。不同的电源需要共地
可以看出舵机其实并不是一种单独的电机它的内部是由直流电机驱动的它里面还有一个控制电路板是一个电机的控制系统。大概的执行逻辑是PWM信号输入到控制板给控制板一个指定的目标角度然后这个电位器检测输出轴的当前角度。如果大于目标角度电机就会反转如果小于目标角度电机就会正转最终使输出轴固定在指定角度这就是舵机的内部工作流程。 棕色是电源负红色是电源正橙色是信号线
直流电机 补充篇 参考资料 电路分析基础6-总说电路的“地”
关于在外设接线中注意的问题——共地 这是因为在实际中各处的零电位实际上是不太相同的将地线接在一起是为了统一零电位以保证各处的电压即电势差有统一的关系。 C语言基础 C语言 区分串口COM口UARTUSART https://blog.csdn.net/qq_26904271/article/details/79829363请跳转这个链接去看这个博主写的挺好的。 图片内容为[1]潘南红,黄连帅,莫秋燕.基于STM32的USART串口异步通信及应用实验设计[J].信息与电脑(理论版),2021,33(19):217-219.
UART和IrDA、LIN的关系 UART和IrDA、LIN的关系
printf函数重定向 USART串口中提到真的牛掰
前置知识
相当于专有名词解释
串行与并行 数字数据通信接口可以分为两大类串行接口和并行接口。
串行通信又称为逐位传输Bit-by-Bit Transmission是指按顺序逐个传输数据位的通信方式。在串行通信中数据位按照顺序逐一传输通过传输线进行数据传输。虽然传输速度较慢但实现简单。串行通信常用于短距离的数据传输如串口、USB接口等。
并行通信是一种同时传输多个数据位的通信方式也称为同时传输多个数据位Word-by-Word Transmission。在并行通信中数据被分成多个并行传输同时通过多个传输线进行数据传输。虽然传输速度快但实现起来较为复杂。并行通信常用于短距离的数据传输如计算机内部数据总线等。
并行数据传输可以将一个完整的字节(单词或更大的数据)一下子从发送器传输到了接收器。如你所料并行接口比串行接口快得多因为并行-串行和串行-并行的解/译码步骤被省略了。而并行传输的缺点是需要足够数量的传输线(导线)来传输单独的数字。
同步与异步通讯
根据通讯的数据同步方式又分为同步和异步两种可以根据通讯过程中是否有使用到时钟信号进行简单的区分。
在同步通讯中收发设备双方会使用一根信号线表示时钟信号在时钟信号的驱动下双方进行协调 同步数据见图 同步通讯 。 通讯中通常双方会统一规定在时钟信号的上升沿或下降沿对数据线进行采样。
同步通信的数据帧组成一般是同步信号若干数据。在最前面是个同步信号接收端接收数据分析出同步信号之后就认为后边的数据都是实际传输的数据了。理论上来说同步通信一个数据帧里面的若干数据的位数是不受限制的。
同步通信中数据之间是不能有间隔的因为双方在同一个时钟下工作这边接收的必然是另一边发送的。在同步信号之后认为所有的数据都是实际数据所以当没有信息要传输是同步信号要填上空字符。
异步通信是一种常用的通信方式发送字符之间的时间间隔可以是任意的。在异步通讯中不使用时钟信号进行数据同步它们直接在数据信号中穿插一些同步用的信号位或者把主体数据进行打包 以数据帧的格式传输数据某些通讯中还需要双方约定数据的传输速率以便更好地同步。
异步通信在发送字符时所发送的字符之间的时间间隔可以是任意的。因为每一帧的数据都有开始和停止位他们之间的数据位才是实际数据。所以接收方评判数据是否为完整的一帧数据的方式就是分析这一堆数据中的开始位和停止位。发送端可以在任意时刻开始发送字符接收端必须时刻做好接收的准备。因为每传输一个数据帧都会有一个开始位和一个停止位实际数据一般只占到5-8位这就导致了异步通信的传输效率较低。 同步与异步通信区别
1.同步通信要求接收端和发送端时钟频率一致而异步通信不要求时钟同步。 2.同步通信效率高异步通信效率较低。 3.同步通信较复杂时钟允许误差较小而异步通信相对简单时钟可允许一定误差。 4.同步通信可用于点对多点而异步通信只适用于点对点。 补充I2C和SPI由于具有独立的时钟线因此它们是同步的。在时钟信号的指引下接收方可以采样数据。然而串口、CAN和USB没有时钟线因此需要双方约定一个采样频率这就是异步通信。为了对齐采样位置还需要添加一些帧头和帧尾等标识。 同步靠时钟线异步靠比特率
通讯速率
衡量通讯性能的一个非常重要的参数就是通讯速率通常以**比特率(Bitrate)**来表示即每秒钟传输的二进制位数 单位为比特每秒(bit/s)。
容易与比特率混淆的概念是“波特率”(Baudrate)它表示每秒钟传输了多少个码元。 而码元是通讯信号调制的概念通讯中常用时间间隔相同的符号来表示一个二进制数字这样的信号称为码元。 如常见的通讯传输中用0V表示数字05V表示数字1那么一个码元可以表示两种状态0和1所以一个码元等于一个二进制比特位 此时波特率的大小与比特率一致如果在通讯传输中有0V、2V、4V以及6V分别表示二进制数00、01、10、11 那么每个码元可以表示四种状态即两个二进制比特位所以码元数是二进制比特位数的一半这个时候的波特率为比特率的一半。
因为很多常见的通讯中一个码元都是表示两种状态人们常常直接以波特率来表示比特率虽然严格来说没什么错误但希望您能了解它们的区别。
在计算机科学里大部分复杂的问题都可以通过分层来简化。如芯片被分为内核层和片上外设STM32标准库则是在寄存器与用户代码之间的软件层。 对于通讯协议我们也以分层的方式来理解最基本的是把它分为物理层和协议层 。物理层规定通讯系统中具有机械、电子功能部分的特性 确保原始数据在物理媒体的传输。协议层主要规定通讯逻辑统一收发双方的数据打包、解包标准。 简单来说物理层规定我们用嘴巴还是用肢体来交流协议层则规定我们用中文还是英文来交流。
USART串口
注意在串口助手的接收模式中有文本模式和HEX模式两种模式那么它们有什么区别
文本模式和Hex模式是两种不同的文件编辑或浏览模式不是完全相同的概念。文本模式通常是指以ASCII编码格式表示文本文件的编辑或浏览模式。在文本模式下文本文件的内容以可读的字符形式显示包括字母、数字、符号等这些字符被转换为计算机能够识别和处理的二进制编码。而Hex模式则是指以十六进制编码格式显示文件内容的编辑或浏览模式。在Hex模式下文件的内容以16进制数值的形式显示每个字节byte用两个十六进制数表示从0x00到0xFF可以查看文件的二进制编码包括数据、指令、标志位等信息。因此虽然文本模式和Hex模式都是用于文件编辑或浏览的模式但它们的显示和处理方式不同用途也不同。
STM32如何才能获取到陀螺仪、蓝牙器等这些外挂模的数据呢
这就需要我们在这两个设备之间连接上一根或多根通信线通过通信线路发送或者接收数据完成数据交换从而实现控制外挂模块和读取外挂模块数据的目的。所以在这里通信的目的是将一个设备的数据传送到另一个设备单片机有了通信的功能就能与众多别的模块互联极大地扩展了硬件系统。
下面我们分别对串口通讯协议的物理层及协议层进行讲解。
物理层
串口通讯的物理层有很多标准及变种我们主要讲解RS-232标准 RS-232标准主要规定了信号的用途、通讯接口以及信号的电平标准。
使用RS-232标准的串口设备间常见的通讯结构见图 串口通讯结构图 。
在上面的通讯方式中两个通讯设备的“DB9接口”之间通过串口信号线建立起连接串口信号线中使用“RS-232标准”传输数据信号。 由于RS-232电平标准的信号不能直接被控制器直接识别所以这些信号会经过一个“电平转换芯片”转换成控制器能识别的“TTL标准”的电平信号才能实现通讯。
电平标准
根据通讯使用的电平标准不同串口通讯可分为TTL标准及RS-232标准见表 TTL电平标准与RS232电平标准 。 使用RS232与TTL电平校准表示同一个信号时的对比见图 RS-232与TTL电平标准下表示同一个信号 。
因为控制器一般使用TTL电平标准所以常常会使用MAX3232芯片对TTL及RS-232电平的信号进行互相转换。
RS-232信号线
在最初的应用中RS-232串口标准常用于计算机、路由与调制调解器(MODEN俗称“猫”)之间的通讯 在这种通讯系统中 设备被分为数据终端设备DTE(计算机、路由)和数据通讯设备DCE(调制调解器)。我们以这种通讯模型讲解它们的信号线连接方式及各个信号线的作用。
在旧式的台式计算机中一般会有RS-232标准的COM口(也称DB9接口)见图 电脑主板上的COM口及串口线.
其中接线口以针式引出信号线的称为公头以孔式引出信号线的称为母头。在计算机中一般引出公头接口而在调制调解器设备中引出的一般为母头使用上图中的串口线即可把它与计算机连接起来。通讯时串口线中传输的信号就是使用前面讲解的RS-232标准调制的。
在这种应用场合下DB9接口中的公头及母头的各个引脚的标准信号线接法见图 DB9标准的公头及母头接法 及表 DB9信号线说明 。 上表中的是计算机端的DB9公头标准接法由于两个通讯设备之间的收发信号(RXD与TXD)应交叉相连 所以调制调解器端的DB9母头的收发信号接法一般与公头的相反两个设备之间连接时只要使用“直通型”的串口线连接起来即可 见图 计算机与调制调解器的信号线连接 。
串口线中的RTS、CTS、DSR、DTR及DCD信号使用逻辑 1表示信号有效逻辑0表示信号无效。 例如当计算机端控制DTR信号线表示为逻辑1时它是为了告知远端的调制调解器本机已准备好接收数据0则表示还没准备就绪。
在目前的其它工业控制使用的串口通讯中一般只使用RXD、TXD以及GND三条信号线 直接传输数据信号而RTS、CTS、DSR、DTR及DCD信号都被裁剪掉了。
协议层
串口通讯的数据包由发送设备通过自身的TXD接口传输到接收设备的RXD接口。在串口通讯的协议层中 规定了数据包的内容它由启始位、主体数据、校验位以及停止位组成通讯双方的数据包格式要约定一致才能正常收发数据 其组成见图 串口数据包的基本组成 。
串口中每一个字节都装载在一个数据帧里面每个数据帧都由起始位、数据位和停止位组成.
波特率 本章中主要讲解的是串口异步通讯异步通讯中由于没有时钟信号(如前面讲解的DB9接口中是没有时钟信号的) 所以两个通讯设备之间需要约定好波特率即每个码元的长度以便对信号进行解码 图 串口数据包的基本组成中用虚线分开的每一格就是代表一个码元。常见的波特率为4800、9600、115200等。
例如如果每隔1秒发送一位那么接收方也必须每隔1秒接收一位。如果接收方过早接收则可能会重复接收某些位如果接收方过晚接收则可能会错过某些位。因此发送方和接收方必须约定好传输速率这个速率参数就是波特率。那反应到波形上比如我们双方规定波特率为1000bps那就表示1s要发1000位每一位的时间就是1ms发送方每隔1ms发送一位接收方每隔1ms接收一位这就是波特率它决定了每隔多久发送一位。
通讯的起始和停止信号
起始位它是标志一个数据帧的开始固定为低电平。首先串口的空闲状态是高电平也就是没有数据传输的时候然后需要传输的时候必须要先发送一个起始位这个起始位必须是低电平来打破空闲状态的高电平产生一个下降沿。这个下降沿就告诉接收设备这一帧数据要开始了。如果没有起始位那当我发送8个1的时候是不是数据线就一直都是高电平没有任何波动对吧。这样接收方怎么知道我发送数据了呢。
同理在一个字节数据发送完成后必须要有一个停止位这个停止位的作用是用于数据帧间隔固定为高电平。同时这个停止位也是为下一个起始位做准备的如果没有停止位那当我数据最后一位是0的时候下次再发送新的一帧是不是就没法产生下降沿了对吧。这就是起始位和停止位的作用。起始位固定为0产生下降沿表示传输开始停止位固定为1把引脚恢复成高电平方便下一次的下降沿如果没有数据了正好引脚也为高电平代表空闲状态。
数据位
这里数据位表示数据帧的有效载荷1为高电平0为低电平低位先行。比如我要发送一个字节是0x0F那就首先把0F转换为二进制就是0000 1111然后低位先行所以数据要从低位开始发送也就是1111 0000像这样依次放在发送引脚上。所以说如果你想发0x0F这一个字节数据那就按照波特率要求定时翻转引脚电平产生一个这样的波形就行了。 有效数据
在数据包的起始位之后紧接着的就是要传输的主体数据内容也称为有效数据有效数据的长度常被约定为5、6、7或8位长。
数据校验
最后看一下校验位它的用途是用于数据验证是根据数据位计算得来的。这里串口使用的是一种叫奇偶校验的数据验证方法奇偶校验可以判断数据传输是不是出错了。如果数据出错了可以选择丢弃或者要求重传校验可以选择3种方式无校验、奇校验和偶校验。无校验就是不需要校验位波形就是左边这个起始位、数据位、停止位总共3个部分。
奇校验要求有效数据和校验位中“1”的个数为奇数比如一个8位长的有效数据为01101001此时总共有4个“1” 为达到奇校验效果校验位为“1”最后传输的数据将是8位的有效数据加上1位的校验位总共9位。
偶校验与奇校验要求刚好相反要求帧数据和校验位中“1”的个数为偶数 比如数据帧11001010此时数据帧“1”的个数为4个所以偶校验位为“0”。
0校验是不管有效数据中的内容是什么校验位总为“0”1校验是校验位总为“1”。
当然奇偶校验的检出率并不是很高比如如果有两位数据同时出错。奇偶特性不变那就校验不出来了所以奇偶校验只能保证一定程度上的数据校验。如果想要更高的检出率可以了解一下CRC校验这个校验会更加好用当然也会更复杂。我们这个STM32内部也有CRC的外设可以了解一下那到这里串口的时序我们就了解了。
说明我们这里的数据位有两种表示方法一种是把校验位作为数据位的一部分分为8位数据和9位数据其中9位数据就是8位有效载荷和1位校验位另一种就是把数据位和校验位独立开数据位就是有效载荷校验位就是独立的1位像我这上面的描述就是把数据位和校验位分开描述了在串口助手里也是分开描述总之无论是合在一起还是分开描述描述的都是同一个东西这个应该也好理解。
串口时序 总结一下就是TX引脚输出定时翻转的高低电平RX引脚定时读取引脚的高低电平。每个字节的数据加上起始位、停止位、可选的校验位打包为数据帧依次输出在TX引脚另一端RX引脚依次接收这样就完成了字节数据的传递这就是串口通信。
STM32的USART串口
另外我们经常还会遇到串口叫UART少了个S就是通用异步收发器一般我们串口很少使用这个同步功能所以USART和UART使用起来也没有什么区别。其实这个STM32的USART同步模式只是多了个时钟输出而已它只支持时钟输出不支持时钟输入所以这个同步模式更多的是为了兼容别的协议或者特殊用途而设计的并不支持两个USART之间进行同步通信。所以我们学习串口主要还是异步通信。
串行通信一般是以帧格式传输数据即是一帧一帧的传输每帧包含有起始信号、数据信息、停止信息 可能还有校验信息。USART就是对这些传输参数有具体规定当然也不是只有唯一一个参数值很多参数值都可以自定义设置只是增强它的兼容性。
我们之前学习了串口的协议串口主要就是靠收发这样的、约定好的波形来进行通信的那这个USART外设就是串口通信的硬件支持电路。
这个同步模式就是多了个时钟CLK的输出硬件流控制比如A设备的TX脚向B设备的RX脚发送数据A设备一直在发发的太快了B处理不过来如果没有硬件流控制那B就只能抛弃新数据或者覆盖原数据了。如果有硬件流控制在硬件电路上会多出一根线如果B没准备好接收就置高电平如果准备好了就置低电平。A接收到了B反馈的准备信号就只会在B准备好的时候才发数据如果B没准备好那数据就不会发送出去。这就是硬件流控制可以防止因为B处理慢而导致数据丢失的问题之后DMA是这个串口支持DMA进行数据转运可以使用DMA转运数据减轻CPU的负担最后智能卡、IrDA、LIN这些是其他的一些协议。因为这些协议和串口是非常的像所以STM32就对USART加了一些小改动就能兼容这么多协议了不过我们一般不用像这些协议Up主也都没用过。
USART框图详解 引脚部分 TX 发送数据输出引脚。
RX 接收数据输入引脚。
SCLK 发送器时钟输出引脚。这个引脚仅适用于同步模式。
下面这里的SWRX、IRDA_OUT/IN这些是智能卡和IrDA通信的引脚我们不用这些协议所以这些引脚就不用管的。
SW_RX 数据接收引脚只用于单线和智能卡模式属于内部引脚没有具体外部引脚。 nRTS 请求以发送(Request To Send)n表示低电平有效。如果使能RTS流控制当USART接收器准备好接收新数据时就会将nRTS变成低电平 当接收寄存器已满时nRTS将被设置为高电平。该引脚只适用于硬件流控制。
nCTS 清除以发送(Clear To Send)n表示低电平有效。如果使能CTS流控制发送器在发送下一帧数据之前会检测nCTS引脚 如果为低电平表示可以发送数据如果为高电平则在发送完当前数据帧之后停止发送。该引脚只适用于硬件流控制。
数据寄存器 USART_DR包含了已发送的数据或者接收到的数据。USART_DR实际是包含了两个寄存器一个专门用于发送的可写TDR 一个专门用于接收的可读RDR。这两个寄存器占用同一个地址在程序上只表现为一个寄存器。当进行发送操作时往USART_DR写入数据会自动存储在TDR内当进行读取操作时向USART_DR读取数据会自动提取RDR数据。
USART数据寄存器(USART_DR)只有低9位有效并且第9位数据是否有效要取决于USART控制寄存器1(USART_CR1)的M位设置 当M位为0时表示8位数据字长当M位为1表示9位数据字长我们一般使用8位数据字长。
TDR和RDR都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的发送时把TDR内容转移到发送移位寄存器 然后把移位寄存器数据每一位发送出去接收时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到RDR。
USART支持DMA传输可以实现高速数据传输具体DMA使用将在DMA章节讲解。
移位寄存器
然后往下看下面是两个移位寄存器一个用于发送一个用于接收。发送移位寄存器的作用就是把一个字节的数据一位一位地移出去正好对应串口协议的波形的数据位。 这两个寄存器是怎么工作的呢图中主要讲的是发送寄存器 注意一下当TXE标志位置1时数据其实还没有发送出去只要数据从TDR转移到发送移位寄存器了TXE就会置1我们就可以写入新的数据了。【就是发送数据寄存器里一直有数据而发送移位寄存器里的数据一旦移位完成那么发送数据寄存器里的数据就会立刻传输进入发送移位寄存器里再次传输】
看一下接收端这里也是类似的。数据从RX引脚通向接收移位寄存器在接收器控制的驱动下一位一位地读取RX电平先放在最高位然后向右移移位8次之后就能接收一个字节了。同样因为串口协议规定是低位先行所以接收移位寄存器是从高位往低位这个方向移动的。之后当一个字节移位完成之后这一个字节的数据就会整体地一下子转移到接收数据寄存器RDR里来在转移的过程中也会置一个标志位叫RXNE RXNot Empty接收数据寄存器非空当我们检测到RXNE置1之后就可以把数据读走了。同样这里也是两个寄存器进行缓存当数据从移位寄存器转移到RDR时就可以直接移位接收下一帧数据了。
这就是USART外设整个的工作流程其实讲到这里这个外设的主要功能就差不多了。大体上就是数据寄存器和移位寄存器发送移位寄存器往TX引脚移位接收移位寄存器从RX引脚移位。当然发送还需要加上帧头帧尾接收还需要剔除帧头帧尾这些操作它内部有电路会自动执行。我们知道有硬件帮我们做了这些工作就行了
接着我们继续看一下下面的控制部分和一些其他的增强功能
硬件流控
下面这里是发送器控制它就是用来控制发送移位寄存器的工作的接收器控制用来控制接收移位寄存器的工作然后左边这里有一个硬件数据流控也就是硬件流控制简称流控。
这里流控有两个引脚一个是nRTS一个是nCTS。nRTSRequest To Send是请求发送是输出脚也就是告诉别人我当前能不能接收nCTS Clear To Send是清除发送是输入脚也就是用于接收别人nRTS的信号的。
这里前面加个n意思是低电平有效那这两个脚上怎么玩的呢
首先我们需要找到一个支持流控的串口并将它的TX连接到我们的RX。同时我们的RTS需要输出一个接收反馈信号并将其连接到对方的CTS。当我们可以接收数据时RTS会置为低电平请求对方发送。对方的CTS接收到信号后就可以继续发送数据。如果处理不过来比如接收数据寄存器未及时读取导致新数据无法接收此时RTS会置为高电平对方的CTS接收到信号后就会暂停发送直到接收数据寄存器被读取RTS重新置为低电平数据才会继续发送。
当我们的TX向对方发送数据时对方的RTS会连接到我们的CTS用于判断对方是否可以接收数据。TX和CTS是一对对应的信号RX和RTS也是一对对应的信号。此外CTS和RTS之间也需要交叉连接这就是流控的工作模式。然而我们一般不使用流控因此只需要了解一下即可。少用原因应该是多消耗两根通信线
SCLK控制 接着继续看右边这个模块这部分电路用于产生同步的时钟信号它是配合发送移位寄存器输出的发送寄存器每移位一次同步时钟电平就跳变一个周期。时钟告诉对方我移出去一位数据你看要不要让我这个时钟信号来指导你接收一下当然这个时钟只支持输出不支持输入所以两个USART之间不能实现同步的串口通信。
那这个时钟信号有什么用呢 兼容别的协议。比如串口加上时钟之后就跟SPI协议特别像,所以有了时钟输出的串口就可以兼容SPI。另外这个时钟也可以做自适应波特率比如接收设备不确定发送设备给的什么波特率然后再计算得到波特率不过这就需要另外写程序来实现这个功能了。这个时钟功能我们一般不用所以也是了解一下就行
唤醒单元 这部分的作用是实现串口挂载多设备。我们之前说串口一般是点对点的通信只支持两个设备互相通信。而多设备在一条总线上可以接多个从设备每个设备分配一个地址我想跟某个设备通信就先进行寻址确定通信对象。那回到这里这个唤醒单元就可以用来实现多设备的功能在这里可以给串口分配一个地址当你发送指定地址时此设备唤醒开始工作当你发送别的设备地址时别的设备就唤醒工作这个设备没收到地址就会保持沉默。这样就可以实现多设备的串口通信了这部分功能我们一般不用。
中断输出控制 中断申请位就是状态寄存器这里的各种标志位状态寄存器这里有两个标志位比较重要一个是TXE发送寄存器空另一个是RXNE接收寄存器非空这两个是判断发送状态和接收状态的必要标志位剩下的标志位了解一下就行。中断输出控制这里就是配置中断是不是能通向NVIC这个应该好理解
波特率发生器部分 波特率发生器其实就是分频器APB时钟进行分频得到发送和接收移位的时钟。看一下这里时钟输入是fPCLKxx1或2USART1挂载在APB2所以就是PCLK2的时钟一般是72M其他的USART都挂载在APB1所以是PCLK1的时钟一般是36M之后这个时钟进行一个分频除一个USARTDIV的分频系数并且分为了整数部分和小数部分因为有些波特率用72M除一个整数的话可能除不尽会有误差。所以这里分频系数是支持小数点后4位的分频就更加精准之后分频完之后还要再除个16得到发送器时钟和接收器时钟通向控制部分。然后右边这里如果TE (TX Enable为1就是发送器使能了发送部分的波特率就有效如果RERX Enable为1就是接收器使能了接收部分的波特率就有效。
然后剩下还有一些寄存器的指示 比如各个CR控制寄存器的哪一位控制哪一部分电路SR状态寄存器都有哪些标志位这些可以自己看看手册里的寄存器描述那里的描述比这里清晰很多
引脚定义表这里复用功能这一栏就给出了每个USART它的各个引脚都是复用在了哪个GPIO上的。 这些引脚都必须按照引脚定义里的规定来或者看一下重映射这里有没有重映射这里有USART1的重映射所以有机会换一次口剩下引脚就没有机会作为USART1的接口了。 USART基本结构 那到这里USART的基本结构就讲完了。
几个小细节 数据帧 这个图是在程序中配置8位字长和9位字长的波形对比。这里的字长就是我们前面说的数据位长度。他这里的字长是包含校验位的是这种描述方式。 总的来说这里有4种选择9位字长有校验或无校验8位字长有校验或无校验。但我们最好选择9位字长 有校验或8位字长 无校验这两种这样每一帧的有效载荷都是1字节这样才舒服。
配置停止位 那最后这些时钟什么的和上面也都是类似的 接下来我们继续来看这个数据帧看一下不同停止位的波形变化。STM32的串口可以配置停止位长度为0.5、1、1.5、2这四种。 这四种参数的区别就是停止位的时长不一样。第一个是1个停止位这时停止位的时长就和数据位的一位时长一样然后是1.5个停止位这时的停止位就是数据位一位时长的1.5倍2个停止位那停止位时长就是2倍0.5个停止位时长就是0.5倍。这个也好理解就是控制停止位时长的一般选择1位停止位就行了其他的参数不太常用。这个是停止位。 起始位侦测和数据采样
那之后我们继续来看一些细节问题这两个图展示的是USART电路输入数据的一些策略。对于串口来说根据我们前面的介绍可以想到串口的输出TX应该是比输入RX简单很多输出你就定时翻转TX引脚高低电平就行了。但是输入就复杂一些。你不仅要保证输入的采样频率和波特率一致还要保证每次输入采样的位置【要正好处于每一位的正中间只有在每一位的正中间采样这样高低电平读进来才是最可靠的如果你采样点过于靠前或靠后那有可能高低电平还正在翻转电平还不稳定或者稍有误差数据就采样错了】。另外输入最好还要对噪声有一定的判断能力如果是噪声最好能置个标志位提醒我一下这些就是输入数据所面临的问题。 那我们来看一下STM32是如何来设计输入电路的呢 第一个图展示了USART的起始位侦测。当输入电路侦测到数据帧的起始位后将以波特率的频率连续采样一帧数据。同时从起始位开始采样位置要对齐到位的正中间。只要第一位对齐了后面就都是对齐的。
为了实现这些功能输入电路对采样时钟进行了细分以波特率的16倍频率进行采样。在一位的时间里可以进行16次采样。比如最开始时空闲状态为高电平采样一直是1。在某个位置突然采到0说明两次采样之间出现了下降沿如果没有噪声那之后就应该是起始位了。在起始位会进行连续16次采样没有噪声的话这16次采样肯定都是0。但是实际电路还是会存在一些噪声所以这里即使出现下降沿了后续也要再采样几次以防万一。
根据手册描述接收电路在下降沿之后的第3次、5次、7次进行一批采样在第8次、9次、10次再进行一批采样。这两批采样都要求每3位里面至少应有2个0。如果没有噪声那肯定全是0满足情况如果有一些轻微的噪声导致3位里面只有两个0另一个是1那也算是检测到了起始位但是在状态寄存器里会置一个NENoise Error提醒你数据收到了但是有噪声你悠着点用如果3位里面只有1个0那就不算检测到了起始位可能前面那个下降沿是噪声导致的这时电路就忽略前面的数据重新开始捕捉下降沿。
这就是STM32的串口在接收过程中对噪声的处理。如果通过了这个起始位侦测那接收状态就由空闲变为接收起始位同时第8、9、10次采样的位置就正好是起始位的正中间。之后接收数据位时就在第8、9、10次进行采样这样就能保证采样位置在位的正中间了。这就是起始位侦测和采样位置对齐的策略。
那紧跟着我们就可以看这个数据采样的流程了。 这里从1到16是一个数据位的时间长度在一个数据位有16个采样时钟由于起始位侦测已经对齐了采样时钟所以这里就直接在第8、9、10次采样数据位。为了保证数据的可靠性这里是连续采样3次没有噪声的理想情况下这3次肯定全为1或者全为0全为1就认为收到了1全为0就认为收到了0如果有噪声导致3次采样不是全为1或者全为0那它就按照21的规则来2次为1就认为收到了12次为0就认为收到了0在这种情况下噪声标志位NE也会置1告诉你我收到数据了但是有噪声你悠着点用这就是检测噪声的数据采样可见STM32对这个电路的设计考虑还是很充分的
波特率发生器 那最后我们再来看一下波特率发生器 为什么这里公式有个16因为它内部还有一个16倍波特率的采样时钟所以这里输入时钟/DV要等于16倍的波特率最终计算波特率自然要多除一个16了。
举个例子比如我要配置USART1为9600的波特率那如何配置这个BRR寄存器呢 我们代入公式就是9600等于 USART1的时钟是72M 除 16倍的DIV解得DIV72M/9600/16最终等于468.75则二进制数是11101 0100.11v。所以最终写到这个寄存器就是整数部分为11101 0100前面多出来的补0小数部分为11后面多出来的补0。这就是根据波特率写BRR寄存器的方法了解一下不过我们用库函数配置的话就非常方便需要多少波特率直接写就行了库函数会自动帮我们算。 手册讲解 USB转串口模块的内部电路图 代码实战串口发送串口发送接受 9-1串口发送
下面这个是我们的USB转串口的模块这里有个跳线帽上节也说过要插在VCC和3V3这两个脚上选择通信的TTL电平为3.3V然后通信引脚TXD和RXD要接在STM32的PA9和PA10口。为什么是这两个口呢我们看一下引脚定义表就知道USART1的TX是PA9, RX是PA10我们计划用USART1进行通信所以就选这两个脚。TX和RX交叉连接这边一定要注意别接错了。然后两个设备之间要把负极接在一起进行共地一般多个系统之间互连都要进行共地。最后这个串口模块和STLINK都要插在电脑上这样STM32和串口模块都有独立供电所以这里通信的电源正极就不需要接了。
当然我们第一个代码只有STM32发送的部分所以通信线只有这个发送的有用另一根线第一个代码没有用到暂时可以不接在我们下一个串口发送接收的代码两根通信线就都需要接了。所以我们把这两根通信线一起都接上吧这样两个代码的接线图是一模一样的。
老规矩上来先写一个初始化函数
第一步开启时钟把需要用的USART和GPIO的时钟打开 第二步GPIO初始化把TX配置成复用输出RX配置成输入 第三步配置USART直接使用一个结构体,就可以把这里所有的参数都配置好了 第四步如果你只需要发送的功能就直接开启USART初始化就结束了。如果你需要接收的功能可能还需要配置中断那就在开启USART之前再加上ITConfig和NVIC的代码就行了。 那初始化完成之后如果要发送数据调用一个发送函数就行了如果要接收数据就调用接收的函数如果要获取发送和接收的状态就调用获取标志位的函数这就是USART外设的使用思路。
Serial.c部分
#include stm32f10x.h // Device header
#include stdio.h
#include stdarg.h/*** 函 数串口初始化* 参 数无* 返 回 值无*/
void Serial_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出/*USART初始化*/USART_InitTypeDef USART_InitStructure; //定义结构体变量USART_InitStructure.USART_BaudRate 9600; //波特率USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; //硬件流控制不需要USART_InitStructure.USART_Mode USART_Mode_Tx; //模式选择为发送模式USART_InitStructure.USART_Parity USART_Parity_No; //奇偶校验不需要USART_InitStructure.USART_StopBits USART_StopBits_1; //停止位选择1位USART_InitStructure.USART_WordLength USART_WordLength_8b; //字长选择8位USART_Init(USART1, USART_InitStructure); //将结构体变量交给USART_Init配置USART1/*USART使能*/USART_Cmd(USART1, ENABLE); //使能USART1串口开始运行
}/*** 函 数串口发送一个字节* 参 数Byte 要发送的一个字节* 返 回 值无*/
void Serial_SendByte(uint8_t Byte)
{USART_SendData(USART1, Byte); //将字节数据写入数据寄存器写入后USART自动生成时序波形while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); //等待发送完成/*下次写入数据寄存器会自动清除发送完成标志位故此循环后无需清除标志位*/
}/*** 函 数串口发送一个数组* 参 数Array 要发送数组的首地址* 参 数Length 要发送数组的长度* 返 回 值无*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{uint16_t i;for (i 0; i Length; i ) //遍历数组{Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据}
}/*** 函 数串口发送一个字符串* 参 数String 要发送字符串的首地址* 返 回 值无*/
void Serial_SendString(char *String)
{uint8_t i;for (i 0; String[i] ! \0; i )//遍历字符数组字符串遇到字符串结束标志位后停止{Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据}
}/*** 函 数次方函数内部使用* 返 回 值返回值等于X的Y次方*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{uint32_t Result 1; //设置结果初值为1while (Y --) //执行Y次{Result * X; //将X累乘到结果}return Result;
}/*** 函 数串口发送数字* 参 数Number 要发送的数字范围0~4294967295* 参 数Length 要发送数字的长度范围0~10* 返 回 值无*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{uint8_t i;for (i 0; i Length; i ) //根据数字长度遍历数字的每一位{Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 0); //依次调用Serial_SendByte发送每位数字}
}/*** 函 数使用printf需要重定向的底层函数* 参 数保持原始格式即可无需变动* 返 回 值保持原始格式即可无需变动*/
int fputc(int ch, FILE *f)
{Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数return ch;
}/*** 函 数自己封装的prinf函数* 参 数format 格式化字符串* 参 数... 可变的参数列表* 返 回 值无*/
void Serial_Printf(char *format, ...)
{char String[100]; //定义字符数组va_list arg; //定义可变参数列表数据类型的变量argva_start(arg, format); //从format开始接收参数列表到arg变量vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中va_end(arg); //结束变量argSerial_SendString(String); //串口发送字符数组字符串
}mian.c部分
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include Serial.hint main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化Serial_Init(); //串口初始化/*串口基本函数*/Serial_SendByte(0x41); //串口发送一个字节数据0x41uint8_t MyArray[] {0x42, 0x43, 0x44, 0x45}; //定义数组Serial_SendArray(MyArray, 4); //串口发送一个数组Serial_SendString(\r\nNum1); //串口发送字符串Serial_SendNumber(111, 3); //串口发送数字/*下述3种方法可实现printf的效果*//*方法1直接重定向printf但printf函数只有一个此方法不能在多处使用*/printf(\r\nNum2%d, 222); //串口发送printf打印的格式化字符串//需要重定向fputc函数并在工程选项里勾选Use MicroLIB/*方法2使用sprintf打印到字符数组再用串口发送字符数组此方法打印到字符数组之后想怎么处理都可以可在多处使用*/char String[100]; //定义字符数组sprintf(String, \r\nNum3%d, 333);//使用sprintf把格式化字符串打印到字符数组Serial_SendString(String); //串口发送字符数组字符串/*方法3将sprintf函数封装起来实现专用的printf此方法就是把方法2封装起来更加简洁实用可在多处使用*/Serial_Printf(\r\nNum4%d, 444); //串口打印字符串使用自己封装的函数实现printf的效果Serial_Printf(\r\n);while (1){}
}9-2 串口发送接受 Serial.c部分
#include stm32f10x.h // Device header
#include stdio.h
#include stdarg.huint8_t Serial_RxData; //定义串口接收的数据变量
uint8_t Serial_RxFlag; //定义串口接收的标志位变量/*** 函 数串口初始化* 参 数无* 返 回 值无*/
void Serial_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure); //将PA10引脚初始化为上拉输入/*USART初始化*/USART_InitTypeDef USART_InitStructure; //定义结构体变量USART_InitStructure.USART_BaudRate 9600; //波特率USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; //硬件流控制不需要USART_InitStructure.USART_Mode USART_Mode_Tx | USART_Mode_Rx; //模式发送模式和接收模式均选择USART_InitStructure.USART_Parity USART_Parity_No; //奇偶校验不需要USART_InitStructure.USART_StopBits USART_StopBits_1; //停止位选择1位USART_InitStructure.USART_WordLength USART_WordLength_8b; //字长选择8位USART_Init(USART1, USART_InitStructure); //将结构体变量交给USART_Init配置USART1/*中断输出配置*/USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断/*NVIC中断分组*/NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2/*NVIC配置*/NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; //选择配置NVIC的USART1线NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; //指定NVIC线路使能NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; //指定NVIC线路的抢占优先级为1NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; //指定NVIC线路的响应优先级为1NVIC_Init(NVIC_InitStructure); //将结构体变量交给NVIC_Init配置NVIC外设/*USART使能*/USART_Cmd(USART1, ENABLE); //使能USART1串口开始运行
}/*** 函 数串口发送一个字节* 参 数Byte 要发送的一个字节* 返 回 值无*/
void Serial_SendByte(uint8_t Byte)
{USART_SendData(USART1, Byte); //将字节数据写入数据寄存器写入后USART自动生成时序波形while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); //等待发送完成/*下次写入数据寄存器会自动清除发送完成标志位故此循环后无需清除标志位*/
}/*** 函 数串口发送一个数组* 参 数Array 要发送数组的首地址* 参 数Length 要发送数组的长度* 返 回 值无*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{uint16_t i;for (i 0; i Length; i ) //遍历数组{Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据}
}/*** 函 数串口发送一个字符串* 参 数String 要发送字符串的首地址* 返 回 值无*/
void Serial_SendString(char *String)
{uint8_t i;for (i 0; String[i] ! \0; i )//遍历字符数组字符串遇到字符串结束标志位后停止{Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据}
}/*** 函 数次方函数内部使用* 返 回 值返回值等于X的Y次方*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{uint32_t Result 1; //设置结果初值为1while (Y --) //执行Y次{Result * X; //将X累乘到结果}return Result;
}/*** 函 数串口发送数字* 参 数Number 要发送的数字范围0~4294967295* 参 数Length 要发送数字的长度范围0~10* 返 回 值无*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{uint8_t i;for (i 0; i Length; i ) //根据数字长度遍历数字的每一位{Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 0); //依次调用Serial_SendByte发送每位数字}
}/*** 函 数使用printf需要重定向的底层函数* 参 数保持原始格式即可无需变动* 返 回 值保持原始格式即可无需变动*/
int fputc(int ch, FILE *f)
{Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数return ch;
}/*** 函 数自己封装的prinf函数* 参 数format 格式化字符串* 参 数... 可变的参数列表* 返 回 值无*/
void Serial_Printf(char *format, ...)
{char String[100]; //定义字符数组va_list arg; //定义可变参数列表数据类型的变量argva_start(arg, format); //从format开始接收参数列表到arg变量vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中va_end(arg); //结束变量argSerial_SendString(String); //串口发送字符数组字符串
}/*** 函 数获取串口接收标志位* 参 数无* 返 回 值串口接收标志位范围0~1接收到数据后标志位置1读取后标志位自动清零*/
uint8_t Serial_GetRxFlag(void)
{if (Serial_RxFlag 1) //如果标志位为1{Serial_RxFlag 0;return 1; //则返回1并自动清零标志位}return 0; //如果标志位为0则返回0
}/*** 函 数获取串口接收的数据* 参 数无* 返 回 值接收的数据范围0~255*/
uint8_t Serial_GetRxData(void)
{return Serial_RxData; //返回接收的数据变量
}/*** 函 数USART1中断函数* 参 数无* 返 回 值无* 注意事项此函数为中断函数无需调用中断触发后自动执行* 函数名为预留的指定名称可以从启动文件复制* 请确保函数名正确不能有任何差异否则中断函数将不能进入*/
void USART1_IRQHandler(void)
{if (USART_GetITStatus(USART1, USART_IT_RXNE) SET) //判断是否是USART1的接收事件触发的中断{Serial_RxData USART_ReceiveData(USART1); //读取数据寄存器存放在接收的数据变量Serial_RxFlag 1; //置接收标志位变量为1USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除USART1的RXNE标志位//读取数据寄存器会自动清除此标志位//如果已经读取了数据寄存器也可以不执行此代码}
}main.c部分
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include Serial.huint8_t RxData; //定义用于接收串口数据的变量int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化/*显示静态字符串*/OLED_ShowString(1, 1, RxData:);/*串口初始化*/Serial_Init(); //串口初始化while (1){if (Serial_GetRxFlag() 1) //检查串口接收数据的标志位{RxData Serial_GetRxData(); //获取串口接收的数据Serial_SendByte(RxData); //串口将收到的数据回传回去用于测试OLED_ShowHexNum(1, 8, RxData, 2); //显示串口接收的数据}}
}USART串口数据包 先来看两张图是关于我规定的数据包格式一种是HEX数据包一种是文本数据包之后两个图展示的就是接收数据包的思路。 接着我们来研究几个问题
第一个问题包头包尾和数据载荷重复的问题这里定义FF为包头FE为包尾如果我传输的数据本身就是FF和FE怎么办呢那这个问题确实存在如果数据和包头包尾重复可能会引起误判。对应这个问题我们有如下几种解决方法第一种限制载荷数据的范围。如果可以的话我们可以在发送的时候对数据进行限幅比如XYZ3个数据变化范围都可以是0~100 那就好办了我们可以在载荷中只发送0-100的数据这样就不会和包头包尾重复了第二种如果无法避免载荷数据和包头包尾重复那我们就尽量使用固定长度的数据包。这样由于载荷数据是固定的只要我们通过包头包尾对齐了数据我们就可以严格知道哪个数据应该是包头包尾哪个数据应该是载荷数据。在接收载荷数据的时候我们并不会判断它是否是包头包尾而在接收包头包尾的时候我们会判断它是不是确实是包头包尾用于数据对齐。这样在经过几个数据包的对齐之后剩下的数据包应该就不会出现问题了第三种增加包头包尾的数量并且尽量让它呈现出载荷数据出现不了的状态。比如我们使用FF、FE作为包头FD、FC作为包尾这样也可以避免载荷数据和包头包尾重复的情况发生
第二个问题这个包头包尾并不是全部都需要的比如我们可以只要一个包头把包尾删掉这样数据包的格式就是一个包头FF加4个数据这样也是可以的。当检测到FF开始接收收够4个字节后置标志位一个数据包接收完成这样也可以。不过这样的话载荷和包头重复的问题会更严重一些比如最严重的情况下我载荷全是FF包头也是FF那你肯定不知道哪个是包头了而加上了FE作为包尾无论数据怎么变化都是可以分辨出包头包尾的。
第三个问题固定包长和可变包长的选择问题对应HEX数据包来说,如果你的载荷会出现和包头包尾重复的情况,那就最好选择固定包长,这样可以避免接收错误,如果你又会重复又选择可变包长那数据很容易就乱套了如果载荷不会和包头包尾重复那可以选择可变包长数据长度像这样4位、3位、等等1位、10位来回任意变肯定都没问题。因为包头包尾是唯一的只要出现包头就开始数据包只要出现包尾就结束数据包这样就非常灵活了这就是固定包长和可变包长选择的问题。
最后一个问题各种数据转换为字节流的问题。这里数据包都是一个字节一个字节组成的如果你想发送16位的整型数据、32位的整型数据float、double甚至是结构体其实都没问题因为它们内部其实都是由一个字节一个字节组成的只需要用一个uint8_t的指针指向它把它们当做一个字节数组发送就行了。
好有关HEX数据包定义的内容就讲这么多接下来看一下文本数据包。 文本数据包和HEX数据包分别对应了文本模式和HEX模式。在HEX数据包中数据以原始字节形式呈现。而在文本数据包中每个字节经过了一层编码和译码最终以文本格式呈现。实际上每个文本字符背后都有一个字节的HEX数据。
综上所述我们需要根据实际场景来选择和设计数据包格式。在需要直接传输和简单解析原始数据的情况下HEX数据包是更好的选择。而在需要输入指令进行人机交互的场合文本数据包则更为适用。
好数据包格式的定义讲完了接下来我们就来学一下数据包的收发流程。
首先发送数据包的过程相对简单。在发送HEX数据包时可以通过定义一个数组填充数据然后使用之前我们写过的SendArray函数发送即可。在发送文本数据包时可以通过写一个字符串然后调用SendString函数发送。因此发送数据包的过程是可控的我们可以根据需要发送任何类型的数据包。相比之下接收数据包的过程较为复杂。
那接下来接收一个数据包这就比较复杂了我们来学习一下我这里演示了固定包长HEX数据包的接收方法和可变包长文本数据包的接收方法其他的数据包也都可以套用这个形式等会儿我们写程序就会根据这里面的流程来。
我们先看一下如何来接收这个固定包长的HEX数据包。要接收固定包长的HEX数据包我们需要设计一个状态机来处理。根据之前的代码我们知道每当收到一个字节程序会进入中断。在中断函数里我们可以获取这个字节但获取后需要退出中断。因此每个收到的数据都是独立的过程而数据包则具有前后关联性包括包头、数据和包尾。为了处理这三种状态我们需要设计一个能够记住不同状态的机制并在不同状态下执行不同的操作同时进行状态合理转移。这种程序设计思维就是“状态机”。
这就是使用状态机接收数据包的思路。这个状态机其实是一种很广泛的编程思路在很多地方都可以用到使用的基本步骤是先根据项目要求定义状态画几个圈然后考虑好各个状态在什么情况下会进行转移如何转移画好线和转移条件最后根据这个图来进行编程这样思维就会非常清晰了。 那接下来继续我们来看一下这个可变包长、文本数据包的接收流程。
好到这里我们这个数据包的定义、分类、优缺点和注意事项就讲完了接下来我们就来写程序验证一下刚才所学的内容吧。
代码实战串口收发HEX数据包串口收发文本数据包
9-3 串口收发HEX数据包 Serial.c部分
#include stm32f10x.h // Device header
#include stdio.h
#include stdarg.huint8_t Serial_TxPacket[4]; //定义发送数据包数组数据包格式FF 01 02 03 04 FE
uint8_t Serial_RxPacket[4]; //定义接收数据包数组
uint8_t Serial_RxFlag; //定义接收数据包标志位/*** 函 数串口初始化* 参 数无* 返 回 值无*/
void Serial_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure); //将PA10引脚初始化为上拉输入/*USART初始化*/USART_InitTypeDef USART_InitStructure; //定义结构体变量USART_InitStructure.USART_BaudRate 9600; //波特率USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; //硬件流控制不需要USART_InitStructure.USART_Mode USART_Mode_Tx | USART_Mode_Rx; //模式发送模式和接收模式均选择USART_InitStructure.USART_Parity USART_Parity_No; //奇偶校验不需要USART_InitStructure.USART_StopBits USART_StopBits_1; //停止位选择1位USART_InitStructure.USART_WordLength USART_WordLength_8b; //字长选择8位USART_Init(USART1, USART_InitStructure); //将结构体变量交给USART_Init配置USART1/*中断输出配置*/USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断/*NVIC中断分组*/NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2/*NVIC配置*/NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; //选择配置NVIC的USART1线NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; //指定NVIC线路使能NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; //指定NVIC线路的抢占优先级为1NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; //指定NVIC线路的响应优先级为1NVIC_Init(NVIC_InitStructure); //将结构体变量交给NVIC_Init配置NVIC外设/*USART使能*/USART_Cmd(USART1, ENABLE); //使能USART1串口开始运行
}/*** 函 数串口发送一个字节* 参 数Byte 要发送的一个字节* 返 回 值无*/
void Serial_SendByte(uint8_t Byte)
{USART_SendData(USART1, Byte); //将字节数据写入数据寄存器写入后USART自动生成时序波形while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); //等待发送完成/*下次写入数据寄存器会自动清除发送完成标志位故此循环后无需清除标志位*/
}/*** 函 数串口发送一个数组* 参 数Array 要发送数组的首地址* 参 数Length 要发送数组的长度* 返 回 值无*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{uint16_t i;for (i 0; i Length; i ) //遍历数组{Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据}
}/*** 函 数串口发送一个字符串* 参 数String 要发送字符串的首地址* 返 回 值无*/
void Serial_SendString(char *String)
{uint8_t i;for (i 0; String[i] ! \0; i )//遍历字符数组字符串遇到字符串结束标志位后停止{Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据}
}/*** 函 数次方函数内部使用* 返 回 值返回值等于X的Y次方*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{uint32_t Result 1; //设置结果初值为1while (Y --) //执行Y次{Result * X; //将X累乘到结果}return Result;
}/*** 函 数串口发送数字* 参 数Number 要发送的数字范围0~4294967295* 参 数Length 要发送数字的长度范围0~10* 返 回 值无*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{uint8_t i;for (i 0; i Length; i ) //根据数字长度遍历数字的每一位{Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 0); //依次调用Serial_SendByte发送每位数字}
}/*** 函 数使用printf需要重定向的底层函数* 参 数保持原始格式即可无需变动* 返 回 值保持原始格式即可无需变动*/
int fputc(int ch, FILE *f)
{Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数return ch;
}/*** 函 数自己封装的prinf函数* 参 数format 格式化字符串* 参 数... 可变的参数列表* 返 回 值无*/
void Serial_Printf(char *format, ...)
{char String[100]; //定义字符数组va_list arg; //定义可变参数列表数据类型的变量argva_start(arg, format); //从format开始接收参数列表到arg变量vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中va_end(arg); //结束变量argSerial_SendString(String); //串口发送字符数组字符串
}/*** 函 数串口发送数据包* 参 数无* 返 回 值无* 说 明调用此函数后Serial_TxPacket数组的内容将加上包头FF包尾FE后作为数据包发送出去*/
void Serial_SendPacket(void)
{Serial_SendByte(0xFF);Serial_SendArray(Serial_TxPacket, 4);Serial_SendByte(0xFE);
}/*** 函 数获取串口接收数据包标志位* 参 数无* 返 回 值串口接收数据包标志位范围0~1接收到数据包后标志位置1读取后标志位自动清零*/
uint8_t Serial_GetRxFlag(void)
{if (Serial_RxFlag 1) //如果标志位为1{Serial_RxFlag 0;return 1; //则返回1并自动清零标志位}return 0; //如果标志位为0则返回0
}/*** 函 数USART1中断函数* 参 数无* 返 回 值无* 注意事项此函数为中断函数无需调用中断触发后自动执行* 函数名为预留的指定名称可以从启动文件复制* 请确保函数名正确不能有任何差异否则中断函数将不能进入*/
void USART1_IRQHandler(void)
{static uint8_t RxState 0; //定义表示当前状态机状态的静态变量static uint8_t pRxPacket 0; //定义表示当前接收数据位置的静态变量if (USART_GetITStatus(USART1, USART_IT_RXNE) SET) //判断是否是USART1的接收事件触发的中断{uint8_t RxData USART_ReceiveData(USART1); //读取数据寄存器存放在接收的数据变量/*使用状态机的思路依次处理数据包的不同部分*//*当前状态为0接收数据包包头*/if (RxState 0){if (RxData 0xFF) //如果数据确实是包头{RxState 1; //置下一个状态pRxPacket 0; //数据包的位置归零}}/*当前状态为1接收数据包数据*/else if (RxState 1){Serial_RxPacket[pRxPacket] RxData; //将数据存入数据包数组的指定位置pRxPacket ; //数据包的位置自增if (pRxPacket 4) //如果收够4个数据{RxState 2; //置下一个状态}}/*当前状态为2接收数据包包尾*/else if (RxState 2){if (RxData 0xFE) //如果数据确实是包尾部{RxState 0; //状态归0Serial_RxFlag 1; //接收数据包标志位置1成功接收一个数据包}}USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位}
}main.c部分
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include Serial.h
#include Key.huint8_t KeyNum; //定义用于接收按键键码的变量int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化Key_Init(); //按键初始化Serial_Init(); //串口初始化/*显示静态字符串*/OLED_ShowString(1, 1, TxPacket);OLED_ShowString(3, 1, RxPacket);/*设置发送数据包数组的初始值用于测试*/Serial_TxPacket[0] 0x01;Serial_TxPacket[1] 0x02;Serial_TxPacket[2] 0x03;Serial_TxPacket[3] 0x04;while (1){KeyNum Key_GetNum(); //获取按键键码if (KeyNum 1) //按键1按下{Serial_TxPacket[0] ; //测试数据自增Serial_TxPacket[1] ;Serial_TxPacket[2] ;Serial_TxPacket[3] ;Serial_SendPacket(); //串口发送数据包Serial_TxPacketOLED_ShowHexNum(2, 1, Serial_TxPacket[0], 2); //显示发送的数据包OLED_ShowHexNum(2, 4, Serial_TxPacket[1], 2);OLED_ShowHexNum(2, 7, Serial_TxPacket[2], 2);OLED_ShowHexNum(2, 10, Serial_TxPacket[3], 2);}if (Serial_GetRxFlag() 1) //如果接收到数据包{OLED_ShowHexNum(4, 1, Serial_RxPacket[0], 2); //显示接收的数据包OLED_ShowHexNum(4, 4, Serial_RxPacket[1], 2);OLED_ShowHexNum(4, 7, Serial_RxPacket[2], 2);OLED_ShowHexNum(4, 10, Serial_RxPacket[3], 2);}}
}9-4 串口收发文本数据包 Serial.c部分
#include stm32f10x.h // Device header
#include stdio.h
#include stdarg.hchar Serial_RxPacket[100]; //定义接收数据包数组数据包格式MSG\r\n
uint8_t Serial_RxFlag; //定义接收数据包标志位/*** 函 数串口初始化* 参 数无* 返 回 值无*/
void Serial_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure); //将PA10引脚初始化为上拉输入/*USART初始化*/USART_InitTypeDef USART_InitStructure; //定义结构体变量USART_InitStructure.USART_BaudRate 9600; //波特率USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; //硬件流控制不需要USART_InitStructure.USART_Mode USART_Mode_Tx | USART_Mode_Rx; //模式发送模式和接收模式均选择USART_InitStructure.USART_Parity USART_Parity_No; //奇偶校验不需要USART_InitStructure.USART_StopBits USART_StopBits_1; //停止位选择1位USART_InitStructure.USART_WordLength USART_WordLength_8b; //字长选择8位USART_Init(USART1, USART_InitStructure); //将结构体变量交给USART_Init配置USART1/*中断输出配置*/USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断/*NVIC中断分组*/NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2/*NVIC配置*/NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; //选择配置NVIC的USART1线NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; //指定NVIC线路使能NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; //指定NVIC线路的抢占优先级为1NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; //指定NVIC线路的响应优先级为1NVIC_Init(NVIC_InitStructure); //将结构体变量交给NVIC_Init配置NVIC外设/*USART使能*/USART_Cmd(USART1, ENABLE); //使能USART1串口开始运行
}/*** 函 数串口发送一个字节* 参 数Byte 要发送的一个字节* 返 回 值无*/
void Serial_SendByte(uint8_t Byte)
{USART_SendData(USART1, Byte); //将字节数据写入数据寄存器写入后USART自动生成时序波形while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); //等待发送完成/*下次写入数据寄存器会自动清除发送完成标志位故此循环后无需清除标志位*/
}/*** 函 数串口发送一个数组* 参 数Array 要发送数组的首地址* 参 数Length 要发送数组的长度* 返 回 值无*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{uint16_t i;for (i 0; i Length; i ) //遍历数组{Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据}
}/*** 函 数串口发送一个字符串* 参 数String 要发送字符串的首地址* 返 回 值无*/
void Serial_SendString(char *String)
{uint8_t i;for (i 0; String[i] ! \0; i )//遍历字符数组字符串遇到字符串结束标志位后停止{Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据}
}/*** 函 数次方函数内部使用* 返 回 值返回值等于X的Y次方*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{uint32_t Result 1; //设置结果初值为1while (Y --) //执行Y次{Result * X; //将X累乘到结果}return Result;
}/*** 函 数串口发送数字* 参 数Number 要发送的数字范围0~4294967295* 参 数Length 要发送数字的长度范围0~10* 返 回 值无*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{uint8_t i;for (i 0; i Length; i ) //根据数字长度遍历数字的每一位{Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 0); //依次调用Serial_SendByte发送每位数字}
}/*** 函 数使用printf需要重定向的底层函数* 参 数保持原始格式即可无需变动* 返 回 值保持原始格式即可无需变动*/
int fputc(int ch, FILE *f)
{Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数return ch;
}/*** 函 数自己封装的prinf函数* 参 数format 格式化字符串* 参 数... 可变的参数列表* 返 回 值无*/
void Serial_Printf(char *format, ...)
{char String[100]; //定义字符数组va_list arg; //定义可变参数列表数据类型的变量argva_start(arg, format); //从format开始接收参数列表到arg变量vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中va_end(arg); //结束变量argSerial_SendString(String); //串口发送字符数组字符串
}/*** 函 数USART1中断函数* 参 数无* 返 回 值无* 注意事项此函数为中断函数无需调用中断触发后自动执行* 函数名为预留的指定名称可以从启动文件复制* 请确保函数名正确不能有任何差异否则中断函数将不能进入*/
void USART1_IRQHandler(void)
{static uint8_t RxState 0; //定义表示当前状态机状态的静态变量static uint8_t pRxPacket 0; //定义表示当前接收数据位置的静态变量if (USART_GetITStatus(USART1, USART_IT_RXNE) SET) //判断是否是USART1的接收事件触发的中断{uint8_t RxData USART_ReceiveData(USART1); //读取数据寄存器存放在接收的数据变量/*使用状态机的思路依次处理数据包的不同部分*//*当前状态为0接收数据包包头*/if (RxState 0){if (RxData Serial_RxFlag 0) //如果数据确实是包头并且上一个数据包已处理完毕{RxState 1; //置下一个状态pRxPacket 0; //数据包的位置归零}}/*当前状态为1接收数据包数据同时判断是否接收到了第一个包尾*/else if (RxState 1){if (RxData \r) //如果收到第一个包尾{RxState 2; //置下一个状态}else //接收到了正常的数据{Serial_RxPacket[pRxPacket] RxData; //将数据存入数据包数组的指定位置pRxPacket ; //数据包的位置自增}}/*当前状态为2接收数据包第二个包尾*/else if (RxState 2){if (RxData \n) //如果收到第二个包尾{RxState 0; //状态归0Serial_RxPacket[pRxPacket] \0; //将收到的字符数据包添加一个字符串结束标志Serial_RxFlag 1; //接收数据包标志位置1成功接收一个数据包}}USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位}
}mian.c部分
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include Serial.h
#include LED.h
#include string.hint main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化LED_Init(); //LED初始化Serial_Init(); //串口初始化/*显示静态字符串*/OLED_ShowString(1, 1, TxPacket);OLED_ShowString(3, 1, RxPacket);while (1){if (Serial_RxFlag 1) //如果接收到数据包{OLED_ShowString(4, 1, );OLED_ShowString(4, 1, Serial_RxPacket); //OLED清除指定位置并显示接收到的数据包/*将收到的数据包与预设的指令对比以此决定将要执行的操作*/if (strcmp(Serial_RxPacket, LED_ON) 0) //如果收到LED_ON指令{LED1_ON(); //点亮LEDSerial_SendString(LED_ON_OK\r\n); //串口回传一个字符串LED_ON_OKOLED_ShowString(2, 1, );OLED_ShowString(2, 1, LED_ON_OK); //OLED清除指定位置并显示LED_ON_OK}else if (strcmp(Serial_RxPacket, LED_OFF) 0) //如果收到LED_OFF指令{LED1_OFF(); //熄灭LEDSerial_SendString(LED_OFF_OK\r\n); //串口回传一个字符串LED_OFF_OKOLED_ShowString(2, 1, );OLED_ShowString(2, 1, LED_OFF_OK); //OLED清除指定位置并显示LED_OFF_OK}else //上述所有条件均不满足即收到了未知指令{Serial_SendString(ERROR_COMMAND\r\n); //串口回传一个字符串ERROR_COMMANDOLED_ShowString(2, 1, );OLED_ShowString(2, 1, ERROR_COMMAND); //OLED清除指定位置并显示ERROR_COMMAND}Serial_RxFlag 0; //处理完成后需要将接收数据包标志位清零否则将无法接收后续数据包}}
}I2C(mpu6050陀螺仪和加速度计)
I2C通信协议正在改动 注通信协议的设计背景 3:00~10:13
I2C 通讯协议(InterIntegrated Circuit)是由Phiilps公司开发的由于它引脚少硬件实现简单可扩展性强 不需要USART、CAN等通讯协议的外部收发设备现在被广泛地使用在系统内多个集成电路(IC)间的通讯。 I2C总线是一种用于芯片之间进行通信的串行总线。它由两条线组成串行时钟线SCL和串行数据线SDA。这种总线允许多个设备在同一条总线上进行通信。
物理层 I2C通讯设备之间的常用连接方式见图
I2C通信协议是一种通用的总线协议。I2C通信协议有以下特征
(1) 它是一个支持设备的总线。“总线”指多个设备共用的信号线。在一个I2C通讯总线中 可连接多个I2C通讯设备支持多个通讯主机及多个通讯从机。 (2) 一个I2C总线只使用两条总线线路一条双向串行数据线(SDA) 一条串行时钟线 (SCL)。数据线即用来表示数据时钟线用于数据收发同步。 (3) 每个连接到总线的设备都有一个独立的地址 主机可以利用这个地址进行不同设备之间的访问。 (4) 总线通过上拉电阻接到电源。当I2C设备空闲时会输出高阻态 而当所有设备都空闲都输出高阻态时由上拉电阻把总线拉成高电平。 (5) 多个主机同时使用总线时为了防止数据冲突 会利用仲裁方式决定由哪个设备占用总线。 (6) 具有三种传输模式标准模式传输速率为100kbit/s 快速模式为400kbit/s 高速模式下可达 3.4Mbit/s但目前大多I2C设备尚不支持高速模式。 (7) 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 SDA数据线在每个SCL的时钟周期传输一位数据SCL为高电平的时候SDA表示的数据有效。 应答信号和非应答信号I2C的数据和地址传输都带响应。
一主多从是指单片机作为主机主导I2C总线的运行。挂在I2C总线上的所有外部模块都是从机只有被主机点名后才能控制I2C总线不能在未经允许的情况下访问I2C总线以防止冲突。这就像在课堂上老师是主机学生是从机。未经点名允许学生不能发言但可以被动地听老师讲课。 另外I2C还支持多主多从模型即多个主机。在多主多从模型中总线上任何一个模块都可以主动跳出来说接下来我就是主机你们都得听我的。这就像在教室里老师正在讲课突然一个学生站起来说打断一下接下来让我来说所有同学听我指挥。但是同一个时间只能有一个人说话这时就相当于发生了总线冲突。在总线冲突时I2C协议会进行仲裁仲裁胜利的一方取得总线控制权失败的一方自动变回从机。由于时钟线也由主机控制所以在多主机的模型下还要进行时钟同步。多主机的情况下协议是比较复杂的。本课程仅使用一主多从模型。 以上是有关I2C的设计背景和基本功能。接下来我们将详细分析I2C如何实现这些功能。 作为一个通信协议I2C必须在硬件和软件上作出规定。硬件上的规定包括电路的连接方式、端口的输入输出模式等软件上的规定包括时序的定义、字节的传输方式、高位先行还是低位先行等。这些硬件和软件的规定结合起来构成了一个完整的通信协议。 协议层 I2C的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
1.I2C基本读写过程 先看看I2C通讯过程的基本结构它的通讯过程见图
接下来我们先看一下12C的硬件规定也就是I2C的硬件电路
I2C的硬件电路
这个图就是I2C的典型电路模型这个模型采用了一主多从的结构。在左侧我们可以看到CPU作为主设备控制着总线并拥有很大的权利。其中主机对SCL线拥有完全的控制权无论何时何地主机都负责掌控SCL线。在空闲状态下主机还可以主动发起对SDA的控制。但是从机发送数据或应答时主机需要将SDA的控制权转交给从机。 接下来我们看到了一系列被控IC它们是挂载在12C总线上的从机设备如姿态传感器、OLED、存储器、时钟模块等。这些从机的权利相对较小。对于SCL时钟线它们在任何时刻都只能被动的读取不允许控制SCL线对于SDA数据线从机也不允许主动发起控制只有在主机发送读取从机的命令后或从机应答时从机才能短暂地取得SDA的控制权。这就是一主多从模型中协议的规定。 然后我们来看接线部分。所有I2C设备的SCL和SDA都连接在一起。主机的SCL线拉出来所有从机的SCL都接在这上面。主机的SDA线也是一样拉出来所有从机的SDA接在这上面。这就是SCL和SDA的接线方式。 那到现在我们先不继续往后看了先忽略这两个电阻那到现在假设我们就这样连接那如何规定每个设备SCL和SDA的输入输出模式呢 由于现在是一主多从结构主机拥有SCL的绝对控制权因此主机的SCL可以配置成推挽输出所有从机的SCL都配置成浮空输入或上拉输入。数据流向为主机发送、所有从机接收。但是到SDA线这里就比较复杂了因为这是半双工协议所以主机的SDA在发送时是输出在接收时是输入。同样地从机的SDA也会在输入和输出之间反复切换。如果能够协调好输入输出的切换时机就没有问题。但是这样做的话如果总线时序没有协调好就极有可能发生两个引脚同时处于输出的状态。如果此时一个引脚输出高电平一个引脚输出低电平就会造成电源短路的情况这是要极力避免的。 为了避免这种情况的发生I2C的设计规定所有设备不输出强上拉的高电平而是采用外置弱上拉电阻加开漏输出的电路结构。这两点规定对应于前面提到的“设备的SCL和SDA均要配置成开漏输出模式”以及“SCL和SDA各添加一个上拉电阻阻值一般为4.7KΩ左右”。对应上面这个图。
所有的设备包括CPU和被控IC它们的引脚内部结构都如上图所示。图左侧展示的是SCL的结构其中SClk代表SCL右侧则是SDA的结构其中DATA代表SDA。引脚的信号输入都可以通过一个数据缓冲器或施密特触发器进行因为输入对电路无影响所以任何设备在任何时刻都可以输入。然而在输出部分采用的是开漏输出的配置。
正常的推挽输出方式如下上面一个开关管连接正极下面一个开关管连接负极。当上面导通时输出高电平下面导通时输出低电平。因为这是通过开关管直接连接到正负极的所以这是强上拉和强下拉的模式。 而开漏输出呢就是去掉这个强上拉的开关管输出低电平时下管导通是强下拉输出高电平时下管断开但是没有上管了此时引脚处于浮空的状态这就是开漏输出。
和这里图示是一样的输出低电平这个开关管导通引脚直接接地是强下拉输出高电平这个开关管断开引脚什么都不接处于浮空状态这样的话所有的设备都只能输出低电平而不能输出高电平为了避免高电平造成的引脚浮空这时就需要在总线外面SCL和SDA各外置一个上拉电阻这是通过一个电阻拉到高电平的所以这是一个弱上拉。 UP主用弹簧和杆子的模型解释这一段硬件电路设计 这样做的好处是
第一完全杜绝了电源短路现象保证电路的安全。你看所有人无论怎么拉杆子或者放手杆子都不会处于一个被同时强拉和强推的状态即使有多个人同时往下拉杆子也没问题 第二避免了引脚模式的频繁切换。开漏加弱上拉的模式同时兼具了输入和输出的功能你要是想输出就去拉杆子或放手操作杆子变化就行了你要是想输入就直接放手然后观察杆子高低就行了因为开漏模式下输出高电平就相当于断开引脚所以在输入之前可以直接输出高电平不需要再切换成输入模式了。 第三就是这个模式会有一个“线与”的现象。就是只要有任意一个或多个设备输出了低电平总线就处于低电平只有所有设备都输出高电平总线才处于高电平。I2C可以利用这个电路特性执行多主机模式下的时钟同步和总线仲裁所以这里SCL虽然在一主多从模式下可以用推挽输出但是它仍然采用了开漏加上拉输出的模式因为在多主机模式下会利用到这个特征。 好以上就是I2C的硬件电路设计那接下来我们就要来学习软件也就是时序的设计了。
I2C时序设计 首先我们来学习一下I2C规定的一些时序基本单元。
起始和终止条件
起始条件是指SCL高电平期间SDA从高电平切换到低电平。在I2C总线处于空闲状态时SCL和SDA都处于高电平状态由外挂的上拉电阻保持。当主机需要数据收发时会首先产生一个起始条件。这个起始条件是SCL保持高电平然后把SDA拉低产生一个下降沿。当从机捕获到这个SCL高电平SDA下降沿信号时就会进行自身的复位等待主机的召唤。之后主机需要将SCL拉低。这样做一方面是占用这个总线另一方面也是为了方便这些基本单元的拼接。这样除了起始和终止条件每个时序单元的SCL都是以低电平开始低电平结束。
终止条件是SCL高电平期间SDA从低电平切换到高电平。SCL先放开并回弹到高电平SDA再放开并回弹高电平产生一个上升沿。这个上升沿触发终止条件同时终止条件之后SCL和SDA都是高电平回归到最初的平静状态。这个起始条件和终止条件就类似串口时序里的起始位和停止位。一个完整的数据帧总是以起始条件开始、终止条件结束。另外起始和终止都是由主机产生的。因此从机必须始终保持双手放开不允许主动跳出来去碰总线。如果允许从机这样做那么就会变成多主机模型不在本节的讨论范围之内。这就是起始条件和终止条件的含义。
发送一个字节 接着继续看在起始条件之后这时就可以紧跟着一个发送一个字节的时序单元如何发送一个字节呢
就这样的流程主机拉低SCL把数据放在SDA上主机松开SCL从机读取SDA的数据在SCL的同步下依次进行主机发送和从机接收循环8次就发送了8位数据也就是一个字节另外注意这里是高位先行所以第一位是一个字节的最高位B7然后依次是次高位B6…
接收一个字节
那我们再继续看最后两个基本单元就是应答机制的设计。
发送应答和接收应答
发一字节收一位收一字节发一位
应用 I2C从机地址 12C的完整时序主要有指定地址写,当前地址读和指定地址读这3种。 首先注意的是我们这个12C是一主多从的模型主机可以访问总线上的任何一个设备那如何发出指令来确定要访问的是哪个设备呢 为了解决这个问题我们需要为每个从设备分配一个唯一的设备地址。这些地址就像是每个设备的名字主机通过发送这些地址来确定要与哪个设备通信。
当主机发送一个地址时所有的从设备都会收到这个地址。但是只有与发送的地址匹配的设备会响应主机的读写操作。 在I2C总线中每个挂载的设备的地址必须是唯一的否则当主机发送一个地址时多个设备响应就会导致混乱。 在12C协议标准中从机设备地址分为7位和10位两种。我们今天主要讨论7位地址因为它们相对简单且应用广泛。
每个I2C设备在出厂时都会被分配一个7位的地址。例如MPU6050的7位地址是1101 000而AT24C02的7位地址是1010 000。不同型号的芯片地址是不同的但相同型号的芯片地址是相同的。 如果多个相同型号的芯片挂载在同一条总线上我们可以通过调整地址的最后几位来解决这个问题。例如MPU6050的地址可以通过ADO引脚来改变而AT24C02的地址可以通过A0、A1、A2引脚来改变。这样即使相同型号的芯片挂载在同一个总线上也可以通过切换地址低位的方式保证每个设备的地址都是唯一的。这就是12C设备的从机地址。
下面时序讲解详情 注意时序里面的RA是接收从机的应答位(Receive Ack, RA)
指定地址写
(Sláve Address R/W) 中最后一位 0W写根据协议规定紧跟着的单元就得是接收从机的应答位Receive Ack RA在这个时刻主机要释放SDA 所以如果单看主机的波形应该是这样
释放SDA之后引脚电平回弹到高电平但是根据协议规定从机要在这个位拉低SDA所以单看从机的波形应该是这样绿色线
该应答的时候从机立刻拽住SDA然后应答结束之后从机再放开SDA那现在综合两者的波形结合线与的特性在主机释放SDA之后由于SDA也被从机拽住了所以主机松手后SDA并没有回弹高电平这个过程就代表从机产生了应答。最终高电平期间主机读取SDA发现是0就说明我进行寻址有人给我应答了。如果主机读取SDA发现是1就说明我进行寻址应答位期间我松手了但是没人拽住它没人给我应答那就直接产生停止条件吧并提示一些信息这就是应答位。 然后这个上升沿就是应答位结束后从机释放SDA产生的从机交出了SDA的控制权因为从机要在低电平尽快变换数据所以这个上升沿和SCL的下降沿几乎是同时发生的。
当前地址读
指定地址读 指定地址读指定地址写当前地址读
Sr (Start Repeat)的意思就是重复起始条件因为指定读写标志位只能是跟着起始条件的第一个字节所以如果想切换读写方向只能再来个起始条件。然后起始条件后重新寻址并且指定读写标志位
代码实战10-1 软件I2C读写MPU6050
由于我们这个代码使用的是软件I2C就是用普通的GPIO口手动翻转电平实现的协议它并不需要STM32内部的外设资源支持所以这里的端口SDASCL其实可以任意指定不局限于这两个端口你也可以SCL接PAOSDA接PB12或者SCL接PA8,SDA接PA9看等等等等接在任意的两个普通的GPIO口就可以。 软件I2C只需要用gpio的读写函数就行了就不用I2C的库函数了。
程序的整体框架
MyI2C.h
#ifndef __MYI2C_H
#define __MYI2C_Hvoid MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);#endifMyI2C.C
#include stm32f10x.h // Device header
#include Delay.h/*引脚配置层*//*** 函 数I2C写SCL引脚电平* 参 数BitValue 协议层传入的当前需要写入SCL的电平范围0~1* 返 回 值无* 注意事项此函数需要用户实现内容当BitValue为0时需要置SCL为低电平当BitValue为1时需要置SCL为高电平*/
void MyI2C_W_SCL(uint8_t BitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue); //根据BitValue设置SCL引脚的电平Delay_us(10); //延时10us防止时序频率超过要求
}/*** 函 数I2C写SDA引脚电平* 参 数BitValue 协议层传入的当前需要写入SDA的电平范围0~0xFF* 返 回 值无* 注意事项此函数需要用户实现内容当BitValue为0时需要置SDA为低电平当BitValue非0时需要置SDA为高电平*/
void MyI2C_W_SDA(uint8_t BitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue); //根据BitValue设置SDA引脚的电平BitValue要实现非0即1的特性Delay_us(10); //延时10us防止时序频率超过要求
}/*** 函 数I2C读SDA引脚电平* 参 数无* 返 回 值协议层需要得到的当前SDA的电平范围0~1* 注意事项此函数需要用户实现内容当前SDA为低电平时返回0当前SDA为高电平时返回1*/
uint8_t MyI2C_R_SDA(void)
{uint8_t BitValue;BitValue GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11); //读取SDA电平Delay_us(10); //延时10us防止时序频率超过要求return BitValue; //返回SDA电平
}/*** 函 数I2C初始化* 参 数无* 返 回 值无* 注意事项此函数需要用户实现内容实现SCL和SDA引脚的初始化*/
void MyI2C_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD;GPIO_InitStructure.GPIO_Pin GPIO_Pin_10 | GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOB, GPIO_InitStructure); //将PB10和PB11引脚初始化为开漏输出/*设置默认电平*/GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11); //设置PB10和PB11引脚初始化后默认为高电平释放总线状态
}/*协议层*//*** 函 数I2C起始* 参 数无* 返 回 值无*/
void MyI2C_Start(void)
{MyI2C_W_SDA(1); //释放SDA确保SDA为高电平MyI2C_W_SCL(1); //释放SCL确保SCL为高电平MyI2C_W_SDA(0); //在SCL高电平期间拉低SDA产生起始信号MyI2C_W_SCL(0); //起始后把SCL也拉低即为了占用总线也为了方便总线时序的拼接
}/*** 函 数I2C终止* 参 数无* 返 回 值无*/
void MyI2C_Stop(void)
{MyI2C_W_SDA(0); //拉低SDA确保SDA为低电平MyI2C_W_SCL(1); //释放SCL使SCL呈现高电平MyI2C_W_SDA(1); //在SCL高电平期间释放SDA产生终止信号
}/*** 函 数I2C发送一个字节* 参 数Byte 要发送的一个字节数据范围0x00~0xFF* 返 回 值无*/
void MyI2C_SendByte(uint8_t Byte)
{uint8_t i;for (i 0; i 8; i ) //循环8次主机依次发送数据的每一位{MyI2C_W_SDA(Byte (0x80 i)); //使用掩码的方式取出Byte的指定一位数据并写入到SDA线MyI2C_W_SCL(1); //释放SCL从机在SCL高电平期间读取SDAMyI2C_W_SCL(0); //拉低SCL主机开始发送下一位数据}
}/*** 函 数I2C接收一个字节* 参 数无* 返 回 值接收到的一个字节数据范围0x00~0xFF*/
uint8_t MyI2C_ReceiveByte(void)
{uint8_t i, Byte 0x00; //定义接收的数据并赋初值0x00此处必须赋初值0x00后面会用到MyI2C_W_SDA(1); //接收前主机先确保释放SDA避免干扰从机的数据发送for (i 0; i 8; i ) //循环8次主机依次接收数据的每一位{MyI2C_W_SCL(1); //释放SCL主机机在SCL高电平期间读取SDAif (MyI2C_R_SDA() 1){Byte | (0x80 i);} //读取SDA数据并存储到Byte变量//当SDA为1时置变量指定位为1当SDA为0时不做处理指定位为默认的初值0MyI2C_W_SCL(0); //拉低SCL从机在SCL低电平期间写入SDA}return Byte; //返回接收到的一个字节数据
}/*** 函 数I2C发送应答位* 参 数Byte 要发送的应答位范围0~10表示应答1表示非应答* 返 回 值无*/
void MyI2C_SendAck(uint8_t AckBit)
{MyI2C_W_SDA(AckBit); //主机把应答位数据放到SDA线MyI2C_W_SCL(1); //释放SCL从机在SCL高电平期间读取应答位MyI2C_W_SCL(0); //拉低SCL开始下一个时序模块
}/*** 函 数I2C接收应答位* 参 数无* 返 回 值接收到的应答位范围0~10表示应答1表示非应答*/
uint8_t MyI2C_ReceiveAck(void)
{uint8_t AckBit; //定义应答位变量MyI2C_W_SDA(1); //接收前主机先确保释放SDA避免干扰从机的数据发送MyI2C_W_SCL(1); //释放SCL主机机在SCL高电平期间读取SDAAckBit MyI2C_R_SDA(); //将应答位存储到变量里MyI2C_W_SCL(0); //拉低SCL开始下一个时序模块return AckBit; //返回定义应答位变量
}函数逻辑
void MyI2C_Start(void)
如果起始条件之前SCL和SDA已经是高电平了那先释放哪一个是一样的效果但是在指定地址读中为了改变读写标志位我们这个Start还要兼容这里的重复起始条件Sr。
Sr最开始SCL是低电平SDA电平不敢确定所以保险起见我们趁SCL是低电平先确保释放SDA再释放SCL这时SDA和SCL都是高电平然后再拉低SDA、拉低SCL这样这个Start就可以兼容起始条件和重复起始条件了。 【如果先释放SCL在SCL高电平期间再释放SDA会被误以为是终止条件这里Sr是需要重新生成一个开始条件即SCL高电平期间SDA从高变低。如果不先拉低SDA就容易造成。SCL高电平期间SDA从低变高。变成结束信号了。】
void MyI2C_Stop(void)
在这里如果Stop开始时那就先释放SCL再释放SDA就行了但是在这个时序单元开始时SDA并不一定是低电平所以为了确保之后释放SDA能产生上升沿我们要在时序单元开始时先拉低SDA然后再释放SCL、释放SDA。
void MyI2C_SendByte(uint8_t Byte)
发送一个字节时序开始时SCL是低电平,实际上除了终止条件SCL以高电平结束,所有的单元我们都会保证SCL以低电平结束这样方便各个单元的拼接。 补充 Byte 0x80 就是用按位与的方式取出数据的某一位或某几位感觉这里准确的讲是检查位是否为1而不是取出最高位
…
MPU6050_Reg.h
#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H#define MPU6050_SMPLRT_DIV 0x19
#define MPU6050_CONFIG 0x1A
#define MPU6050_GYRO_CONFIG 0x1B
#define MPU6050_ACCEL_CONFIG 0x1C#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_ACCEL_XOUT_L 0x3C
#define MPU6050_ACCEL_YOUT_H 0x3D
#define MPU6050_ACCEL_YOUT_L 0x3E
#define MPU6050_ACCEL_ZOUT_H 0x3F
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_TEMP_OUT_H 0x41
#define MPU6050_TEMP_OUT_L 0x42
#define MPU6050_GYRO_XOUT_H 0x43
#define MPU6050_GYRO_XOUT_L 0x44
#define MPU6050_GYRO_YOUT_H 0x45
#define MPU6050_GYRO_YOUT_L 0x46
#define MPU6050_GYRO_ZOUT_H 0x47
#define MPU6050_GYRO_ZOUT_L 0x48#define MPU6050_PWR_MGMT_1 0x6B
#define MPU6050_PWR_MGMT_2 0x6C
#define MPU6050_WHO_AM_I 0x75#endifMPU6050.h
#ifndef __MPU6050_H
#define __MPU6050_Hvoid MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);#endifMPU6050.c
#include stm32f10x.h // Device header
#include MyI2C.h
#include MPU6050_Reg.h#define MPU6050_ADDRESS 0xD0 //MPU6050的I2C从机地址/*** 函 数MPU6050写寄存器* 参 数RegAddress 寄存器地址范围参考MPU6050手册的寄存器描述* 参 数Data 要写入寄存器的数据范围0x00~0xFF* 返 回 值无*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{MyI2C_Start(); //I2C起始MyI2C_SendByte(MPU6050_ADDRESS); //发送从机地址读写位为0表示即将写入MyI2C_ReceiveAck(); //接收应答MyI2C_SendByte(RegAddress); //发送寄存器地址MyI2C_ReceiveAck(); //接收应答MyI2C_SendByte(Data); //发送要写入寄存器的数据MyI2C_ReceiveAck(); //接收应答MyI2C_Stop(); //I2C终止
}/*** 函 数MPU6050读寄存器* 参 数RegAddress 寄存器地址范围参考MPU6050手册的寄存器描述* 返 回 值读取寄存器的数据范围0x00~0xFF*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{uint8_t Data;MyI2C_Start(); //I2C起始MyI2C_SendByte(MPU6050_ADDRESS); //发送从机地址读写位为0表示即将写入MyI2C_ReceiveAck(); //接收应答MyI2C_SendByte(RegAddress); //发送寄存器地址MyI2C_ReceiveAck(); //接收应答MyI2C_Start(); //I2C重复起始MyI2C_SendByte(MPU6050_ADDRESS | 0x01); //发送从机地址读写位为1表示即将读取MyI2C_ReceiveAck(); //接收应答Data MyI2C_ReceiveByte(); //接收指定寄存器的数据MyI2C_SendAck(1); //发送应答给从机非应答终止从机的数据输出MyI2C_Stop(); //I2C终止return Data;
}/*** 函 数MPU6050初始化* 参 数无* 返 回 值无*/
void MPU6050_Init(void)
{MyI2C_Init(); //先初始化底层的I2C/*MPU6050寄存器初始化需要对照MPU6050手册的寄存器描述配置此处仅配置了部分重要的寄存器*/MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01); //电源管理寄存器1取消睡眠模式选择时钟源为X轴陀螺仪MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00); //电源管理寄存器2保持默认值0所有轴均不待机MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09); //采样率分频寄存器配置采样率MPU6050_WriteReg(MPU6050_CONFIG, 0x06); //配置寄存器配置DLPFMPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18); //陀螺仪配置寄存器选择满量程为±2000°/sMPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18); //加速度计配置寄存器选择满量程为±16g
}/*** 函 数MPU6050获取ID号* 参 数无* 返 回 值MPU6050的ID号*/
uint8_t MPU6050_GetID(void)
{return MPU6050_ReadReg(MPU6050_WHO_AM_I); //返回WHO_AM_I寄存器的值
}/*** 函 数MPU6050获取数据* 参 数AccX AccY AccZ 加速度计X、Y、Z轴的数据使用输出参数的形式返回范围-32768~32767* 参 数GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据使用输出参数的形式返回范围-32768~32767* 返 回 值无*/
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{uint8_t DataH, DataL; //定义数据高8位和低8位的变量DataH MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); //读取加速度计X轴的高8位数据DataL MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); //读取加速度计X轴的低8位数据*AccX (DataH 8) | DataL; //数据拼接通过输出参数返回DataH MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); //读取加速度计Y轴的高8位数据DataL MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L); //读取加速度计Y轴的低8位数据*AccY (DataH 8) | DataL; //数据拼接通过输出参数返回DataH MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H); //读取加速度计Z轴的高8位数据DataL MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L); //读取加速度计Z轴的低8位数据*AccZ (DataH 8) | DataL; //数据拼接通过输出参数返回DataH MPU6050_ReadReg(MPU6050_GYRO_XOUT_H); //读取陀螺仪X轴的高8位数据DataL MPU6050_ReadReg(MPU6050_GYRO_XOUT_L); //读取陀螺仪X轴的低8位数据*GyroX (DataH 8) | DataL; //数据拼接通过输出参数返回DataH MPU6050_ReadReg(MPU6050_GYRO_YOUT_H); //读取陀螺仪Y轴的高8位数据DataL MPU6050_ReadReg(MPU6050_GYRO_YOUT_L); //读取陀螺仪Y轴的低8位数据*GyroY (DataH 8) | DataL; //数据拼接通过输出参数返回DataH MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); //读取陀螺仪Z轴的高8位数据DataL MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); //读取陀螺仪Z轴的低8位数据*GyroZ (DataH 8) | DataL; //数据拼接通过输出参数返回
}
main.c
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include MPU6050.huint8_t ID; //定义用于存放ID号的变量
int16_t AX, AY, AZ, GX, GY, GZ; //定义用于存放各个数据的变量int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化MPU6050_Init(); //MPU6050初始化/*显示ID号*/OLED_ShowString(1, 1, ID:); //显示静态字符串ID MPU6050_GetID(); //获取MPU6050的ID号OLED_ShowHexNum(1, 4, ID, 2); //OLED显示ID号while (1){MPU6050_GetData(AX, AY, AZ, GX, GY, GZ); //获取MPU6050的数据OLED_ShowSignedNum(2, 1, AX, 5); //OLED显示数据OLED_ShowSignedNum(3, 1, AY, 5);OLED_ShowSignedNum(4, 1, AZ, 5);OLED_ShowSignedNum(2, 8, GX, 5);OLED_ShowSignedNum(3, 8, GY, 5);OLED_ShowSignedNum(4, 8, GZ, 5);}
}那之前的课程我们用的是软件I2C手动拉低或释放时钟线然后再手动对每个数据位进行判断拉低或释放数据线这样来产生这个的波形这是软件I2C。由于12C是同步时序这每一位的持续时间要求不严格或许中途暂停一下时序影响都不大所以2C是比较容易用软件模拟的。 在实际项目中软件模拟的I2C也是非常常见的但是作为一个协议标准I2C通信也是可以有硬件收发电路的。就像之前的串口通信一样我们先讲了串口的时序波形但是在程序中我们并没有用软件去手动翻转电平来实现这个波形这是因为串口是异步时序每一位的时间要求很严格不能过长也不能过短所以串口时序虽然可以用软件模拟但是操作起来比较困难。另外由于串口的硬件收发器在单片机中的普及程度非常高基本上每个单片机都有串口的硬件资源而且硬件实现的串口使用起来还非常简单所以串口通信我们基本都是借助硬件收发器来实现的。
I2C通信外设 硬件实现串口USART的使用流程首先配置USART外设然后写入数据寄存器DR然后硬件收发器就会自动生成波形发送出去最后我们等待发送完成的标志位即可。 回到I2C这里I2C也可以有软件模拟和硬件收发器自动操作这两种异步时序对于串口这样的异步时序软件实现麻烦硬件实现简单所以串口的实现基本是全部倒向硬件。而对于I2C这样的同步时序来说软件实现简单灵活硬件实现麻烦但可以节省软件资源、可以实现完整的多主机通信模型等各有优缺点。
I2C框图
I2C基本结构
代码实战10-2硬件I2C读写MPU6050 MPU6050.h
#ifndef __MPU6050_H
#define __MPU6050_Hvoid MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);#endifMPU6050_REG.h
#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H#define MPU6050_SMPLRT_DIV 0x19
#define MPU6050_CONFIG 0x1A
#define MPU6050_GYRO_CONFIG 0x1B
#define MPU6050_ACCEL_CONFIG 0x1C#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_ACCEL_XOUT_L 0x3C
#define MPU6050_ACCEL_YOUT_H 0x3D
#define MPU6050_ACCEL_YOUT_L 0x3E
#define MPU6050_ACCEL_ZOUT_H 0x3F
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_TEMP_OUT_H 0x41
#define MPU6050_TEMP_OUT_L 0x42
#define MPU6050_GYRO_XOUT_H 0x43
#define MPU6050_GYRO_XOUT_L 0x44
#define MPU6050_GYRO_YOUT_H 0x45
#define MPU6050_GYRO_YOUT_L 0x46
#define MPU6050_GYRO_ZOUT_H 0x47
#define MPU6050_GYRO_ZOUT_L 0x48#define MPU6050_PWR_MGMT_1 0x6B
#define MPU6050_PWR_MGMT_2 0x6C
#define MPU6050_WHO_AM_I 0x75#endifMPU6050.c
#include stm32f10x.h // Device header
#include MPU6050_Reg.h#define MPU6050_ADDRESS 0xD0 //MPU6050的I2C从机地址/*** 函 数MPU6050等待事件* 参 数同I2C_CheckEvent* 返 回 值无*/
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{uint32_t Timeout;Timeout 10000; //给定超时计数时间while (I2C_CheckEvent(I2Cx, I2C_EVENT) ! SUCCESS) //循环等待指定事件{Timeout --; //等待时计数值自减if (Timeout 0) //自减到0后等待超时{/*超时的错误处理代码可以添加到此处*/break; //跳出等待不等了}}
}/*** 函 数MPU6050写寄存器* 参 数RegAddress 寄存器地址范围参考MPU6050手册的寄存器描述* 参 数Data 要写入寄存器的数据范围0x00~0xFF* 返 回 值无*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成起始条件MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //硬件I2C发送从机地址方向为发送MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6I2C_SendData(I2C2, RegAddress); //硬件I2C发送寄存器地址MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING); //等待EV8I2C_SendData(I2C2, Data); //硬件I2C发送数据MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2I2C_GenerateSTOP(I2C2, ENABLE); //硬件I2C生成终止条件
}/*** 函 数MPU6050读寄存器* 参 数RegAddress 寄存器地址范围参考MPU6050手册的寄存器描述* 返 回 值读取寄存器的数据范围0x00~0xFF*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{uint8_t Data;I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成起始条件MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //硬件I2C发送从机地址方向为发送MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6I2C_SendData(I2C2, RegAddress); //硬件I2C发送寄存器地址MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成重复起始条件MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver); //硬件I2C发送从机地址方向为接收MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //等待EV6I2C_AcknowledgeConfig(I2C2, DISABLE); //在接收最后一个字节之前提前将应答失能I2C_GenerateSTOP(I2C2, ENABLE); //在接收最后一个字节之前提前申请停止条件MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED); //等待EV7Data I2C_ReceiveData(I2C2); //接收数据寄存器I2C_AcknowledgeConfig(I2C2, ENABLE); //将应答恢复为使能为了不影响后续可能产生的读取多字节操作return Data;
}/*** 函 数MPU6050初始化* 参 数无* 返 回 值无*/
void MPU6050_Init(void)
{/*开启时钟*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE); //开启I2C2的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_OD;GPIO_InitStructure.GPIO_Pin GPIO_Pin_10 | GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOB, GPIO_InitStructure); //将PB10和PB11引脚初始化为复用开漏输出/*I2C初始化*/I2C_InitTypeDef I2C_InitStructure; //定义结构体变量I2C_InitStructure.I2C_Mode I2C_Mode_I2C; //模式选择为I2C模式I2C_InitStructure.I2C_ClockSpeed 50000; //时钟速度选择为50KHzI2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; //时钟占空比选择Tlow/Thigh 2I2C_InitStructure.I2C_Ack I2C_Ack_Enable; //应答选择使能I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; //应答地址选择7位从机模式下才有效I2C_InitStructure.I2C_OwnAddress1 0x00; //自身地址从机模式下才有效I2C_Init(I2C2, I2C_InitStructure); //将结构体变量交给I2C_Init配置I2C2/*I2C使能*/I2C_Cmd(I2C2, ENABLE); //使能I2C2开始运行/*MPU6050寄存器初始化需要对照MPU6050手册的寄存器描述配置此处仅配置了部分重要的寄存器*/MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01); //电源管理寄存器1取消睡眠模式选择时钟源为X轴陀螺仪MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00); //电源管理寄存器2保持默认值0所有轴均不待机MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09); //采样率分频寄存器配置采样率MPU6050_WriteReg(MPU6050_CONFIG, 0x06); //配置寄存器配置DLPFMPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18); //陀螺仪配置寄存器选择满量程为±2000°/sMPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18); //加速度计配置寄存器选择满量程为±16g
}/*** 函 数MPU6050获取ID号* 参 数无* 返 回 值MPU6050的ID号*/
uint8_t MPU6050_GetID(void)
{return MPU6050_ReadReg(MPU6050_WHO_AM_I); //返回WHO_AM_I寄存器的值
}/*** 函 数MPU6050获取数据* 参 数AccX AccY AccZ 加速度计X、Y、Z轴的数据使用输出参数的形式返回范围-32768~32767* 参 数GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据使用输出参数的形式返回范围-32768~32767* 返 回 值无*/
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{uint8_t DataH, DataL; //定义数据高8位和低8位的变量DataH MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); //读取加速度计X轴的高8位数据DataL MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); //读取加速度计X轴的低8位数据*AccX (DataH 8) | DataL; //数据拼接通过输出参数返回DataH MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); //读取加速度计Y轴的高8位数据DataL MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L); //读取加速度计Y轴的低8位数据*AccY (DataH 8) | DataL; //数据拼接通过输出参数返回DataH MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H); //读取加速度计Z轴的高8位数据DataL MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L); //读取加速度计Z轴的低8位数据*AccZ (DataH 8) | DataL; //数据拼接通过输出参数返回DataH MPU6050_ReadReg(MPU6050_GYRO_XOUT_H); //读取陀螺仪X轴的高8位数据DataL MPU6050_ReadReg(MPU6050_GYRO_XOUT_L); //读取陀螺仪X轴的低8位数据*GyroX (DataH 8) | DataL; //数据拼接通过输出参数返回DataH MPU6050_ReadReg(MPU6050_GYRO_YOUT_H); //读取陀螺仪Y轴的高8位数据DataL MPU6050_ReadReg(MPU6050_GYRO_YOUT_L); //读取陀螺仪Y轴的低8位数据*GyroY (DataH 8) | DataL; //数据拼接通过输出参数返回DataH MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); //读取陀螺仪Z轴的高8位数据DataL MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); //读取陀螺仪Z轴的低8位数据*GyroZ (DataH 8) | DataL; //数据拼接通过输出参数返回
}main.c
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include MPU6050.huint8_t ID; //定义用于存放ID号的变量
int16_t AX, AY, AZ, GX, GY, GZ; //定义用于存放各个数据的变量int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化MPU6050_Init(); //MPU6050初始化/*显示ID号*/OLED_ShowString(1, 1, ID:); //显示静态字符串ID MPU6050_GetID(); //获取MPU6050的ID号OLED_ShowHexNum(1, 4, ID, 2); //OLED显示ID号while (1){MPU6050_GetData(AX, AY, AZ, GX, GY, GZ); //获取MPU6050的数据OLED_ShowSignedNum(2, 1, AX, 5); //OLED显示数据OLED_ShowSignedNum(3, 1, AY, 5);OLED_ShowSignedNum(4, 1, AZ, 5);OLED_ShowSignedNum(2, 8, GX, 5);OLED_ShowSignedNum(3, 8, GY, 5);OLED_ShowSignedNum(4, 8, GZ, 5);}
}SPI协议
10.1 SPI简介
SPISerial Peripheral Interface是由Motorola公司开发的一种通用数据总线 四根通信线SCKSerial Clock、MOSIMaster Output Slave Input、MISOMaster Input Slave Output、SSSlave Select有几个从机就可以直接连接SS 同步全双工 支持总线挂载多设备一主多从 硬件电路 所有SPI设备的SCK、MOSI、MISO分别连在一起 主机另外引出多条SS控制线分别接到各从机的SS引脚 输出引脚配置为推挽输出输入引脚配置为浮空或上拉输入 移位示意图 SPI时序基本单元 模式0最常用
发送指令向SS指定的设备发送指令0x06指令码(读写)地址 指定地址写 向SS指定的设备发送写指令0x02随后在指定地址Address[23:0]下写入指定数据 Data 指定地址读 向SS指定的设备发送读指令0x03随后在指定地址Address[23:0]下读取从机数据 Data
W25Q64简介
W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器常应用于数据存储、字库存 储、固件程序存储等场景 存储介质Nor Flash闪存 时钟频率80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI) 存储容量24位地址
W25Q404Mbit / 512KByte W25Q808Mbit / 1MByte W25Q1616Mbit / 2MByte W25Q3232Mbit / 4MByte W25Q6464Mbit / 8MByte W25Q128128Mbit / 16MByte W25Q256256Mbit / 32MByte
硬件电路 W25Q64框图 Flash操作注意事项
写入操作时
写入操作前必须先进行写使能每个数据位只能由1改写为0不能由0改写为1写入数据前必须先擦除擦除后所有数据位变为1擦除必须按最小擦除单元进行(一个扇区4096 bytes) 可以先读出来再擦除再写入连续写入多字节时最多写入一页(256 bytes)的数据超过页尾位置的数据会回到页首覆盖写入写入操作结束后芯片进入忙状态不响应新的读写操作busy 1
读取操作时
直接调用读取时序无需使能无需额外操作没有页的限制读取操作结束后不会进入忙状态但不能在忙状态时读取
10.3 SPI软件读写W25Q64
接线图 代码 main.c
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include W25Q64.h
uint8_t MID;
uint16_t DID;
uint8_t ArrayWrite[] {0x01, 0x02, 0x03, 0x04};
uint8_t ArrayRead[4];
int main(void)
{OLED_Init();W25Q64_Init();OLED_ShowString(1, 1, MID: DID:);OLED_ShowString(2, 1, W:);OLED_ShowString(3, 1, R:);W25Q64_ReadID(MID, DID);OLED_ShowHexNum(1, 5, MID, 2);OLED_ShowHexNum(1, 12, DID, 4);W25Q64_SectorErase(0x000000);W25Q64_PageProgram(0x000000, ArrayWrite, 4);W25Q64_ReadData(0x000000, ArrayRead, 4);OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);OLED_ShowHexNum(3, 3, ArrayRead[0], 2);OLED_ShowHexNum(3, 6, ArrayRead[1], 2);OLED_ShowHexNum(3, 9, ArrayRead[2], 2);OLED_ShowHexNum(3, 12, ArrayRead[3], 2);while (1){}
}
W25Q64.c
#include stm32f10x.h // Device header
#include MySPI.h
#include W25Q64_Ins.h
void W25Q64_Init(void)
{MySPI_Init();
}
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{MySPI_Start();MySPI_SwapBytes(W25Q64_JEDEC_ID); // 指定读取地址厂商ID设备ID在这里0x9F 指令表*MID MySPI_SwapBytes(W25Q64_DUMMY_BYTE); // 第一个时序先读到MID0xFF 用于去交换数据*DID MySPI_SwapBytes(W25Q64_DUMMY_BYTE); // 第二个时序读到DID的高八位*DID 8; // 将高八位移至高八位*DID | MySPI_SwapBytes(W25Q64_DUMMY_BYTE); // 将低八位移至低八位MySPI_Stop();
}
void W25Q64_WriteEnable(void)
{MySPI_Start();MySPI_SwapBytes(W25Q64_WRITE_ENABLE);MySPI_Stop();
}
void W25Q64_WaitBusy(void)
{uint32_t Timeout;MySPI_Start();MySPI_SwapBytes(W25Q64_READ_STATUS_REGISTER_1);Timeout 100000;while ((MySPI_SwapBytes(W25Q64_DUMMY_BYTE) 0x01) 0x01){Timeout--;if (Timeout 0){break;}}MySPI_Stop();
}
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable();MySPI_Start();MySPI_SwapBytes(W25Q64_PAGE_PROGRAM);MySPI_SwapBytes(Address 16);MySPI_SwapBytes(Address 8);MySPI_SwapBytes(Address);for (i 0; i Count; i){MySPI_SwapBytes(DataArray[i]);}MySPI_Stop();W25Q64_WaitBusy();
}
void W25Q64_SectorErase(uint32_t Address)
{W25Q64_WriteEnable();MySPI_Start();MySPI_SwapBytes(W25Q64_SECTOR_ERASE_4KB);MySPI_SwapBytes(Address 16);MySPI_SwapBytes(Address 8);MySPI_SwapBytes(Address);MySPI_Stop();W25Q64_WaitBusy();
}
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{uint32_t i;MySPI_Start();MySPI_SwapBytes(W25Q64_READ_DATA);MySPI_SwapBytes(Address 16);MySPI_SwapBytes(Address 8);MySPI_SwapBytes(Address);for (i 0; i Count; i){DataArray[i] MySPI_SwapBytes(W25Q64_DUMMY_BYTE);}MySPI_Stop();
}
10.4 SPI硬件读写W25Q64
SPI外设简介
STM32内部集成了硬件SPI收发电路可以由硬件自动执行时钟生成、数据收发等功能减轻CPU 的负担 可配置8位/16位数据帧、高位先行/低位先行 时钟频率fPCLK / (2, 4, 8, 16, 32, 64, 128, 256) 支持多主机模型、主或从操作 可精简为半双工/单工通信 支持DMA 兼容I2S协议(数字音频传输协议) STM32F103C8T6 硬件SPI资源SPI1、SPI2
SPI框图 SPI基本结构 主模式全双工连续传输 非连续传输 软件/硬件波形对比 接线图 常用代码
void SPI_I2S_DeInit(SPI_TypeDef *SPIx);
void SPI_Init(SPI_TypeDef *SPIx, SPI_InitTypeDef *SPI_InitStruct);
void I2S_Init(SPI_TypeDef *SPIx, I2S_InitTypeDef *I2S_InitStruct);
void SPI_StructInit(SPI_InitTypeDef *SPI_InitStruct);
void I2S_StructInit(I2S_InitTypeDef *I2S_InitStruct);
void SPI_Cmd(SPI_TypeDef *SPIx, FunctionalState NewState);
void I2S_Cmd(SPI_TypeDef *SPIx, FunctionalState NewState);
void SPI_I2S_ITConfig(SPI_TypeDef *SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);
void SPI_I2S_DMACmd(SPI_TypeDef *SPIx, uint16_t SPI_I2S_DMAReq,FunctionalState NewState);
void SPI_I2S_SendData(SPI_TypeDef *SPIx, uint16_t Data); // 写DR
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef *SPIx); // 读DR
// 状态标志
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef *SPIx, uint16_t SPI_I2S_FLAG);
void SPI_I2S_ClearFlag(SPI_TypeDef *SPIx, uint16_t SPI_I2S_FLAG);
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef *SPIx, uint8_t SPI_I2S_IT);
void SPI_I2S_ClearITPendingBit(SPI_TypeDef *SPIx, uint8_t SPI_I2S_IT); 代码: main.c
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include W25Q64.h
uint8_t MID;
uint16_t DID;
uint8_t ArrayWrite[] {0x01, 0x02, 0x03, 0x04};
uint8_t ArrayRead[4];
int main(void)
{OLED_Init();W25Q64_Init();OLED_ShowString(1, 1, MID: DID:);OLED_ShowString(2, 1, W:);OLED_ShowString(3, 1, R:);W25Q64_ReadID(MID, DID);OLED_ShowHexNum(1, 5, MID, 2);OLED_ShowHexNum(1, 12, DID, 4);W25Q64_SectorErase(0x000000);W25Q64_PageProgram(0x000000, ArrayWrite, 4);W25Q64_ReadData(0x000000, ArrayRead, 4);OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);OLED_ShowHexNum(3, 3, ArrayRead[0], 2);OLED_ShowHexNum(3, 6, ArrayRead[1], 2);OLED_ShowHexNum(3, 9, ArrayRead[2], 2);OLED_ShowHexNum(3, 12, ArrayRead[3], 2);while (1){}
}SPI.c
#include stm32f10x.h // Device header
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
void MySPI_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);// SS推挽输出GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Pin GPIO_Pin_4;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure);// SCK、MOSI复用推挽输出GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin GPIO_Pin_6;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure);SPI_InitTypeDef SPI_InitStructure;SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_128;// 128分频SPI_InitStructure.SPI_CPOL SPI_CPOL_Low;// 模式0 0-0SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge;// 第一个边沿采样SPI_InitStructure.SPI_CRCPolynomial 7;// 默认7SPI_InitStructure.SPI_DataSize SPI_DataSize_8b;// 8位传输SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex;// 全双工SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB;// 高位先行SPI_InitStructure.SPI_Mode SPI_Mode_Master;// 主模式SPI_InitStructure.SPI_NSS SPI_NSS_Soft;// 软件模拟NSSSPI_Init(SPI1, SPI_InitStructure);SPI_Cmd(SPI1, ENABLE);MySPI_W_SS(0); // 模式0默认高电平默认不选择从机
}
void MySPI_Start(void)
{MySPI_W_SS(0);
}
void MySPI_Stop(void)
{MySPI_W_SS(1);
}
// 四部交换数据1个字节不需要手动清除标志
uint8_t MySPI_SwapBytes(uint8_t ByteSend)
{// 1.等待TXE 1 卡死的概率不大while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) ! SET);// 2.发送数据SPI_I2S_SendData(SPI1, ByteSend);// 3.等待RXNE 1 代表传输完一个字节while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) ! SET);// 4.接收数据return SPI_I2S_ReceiveData(SPI1);
}
BKP、RTC
11.0 Unix时间戳
Unix 时间戳Unix Timestamp定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒 数不考虑闰秒时间戳存储在一个秒计数器中秒计数器为32位/64位的整型变量世界上所有时区的秒计数器相同不同时区通过添加偏移来得到当地时间
UTC/GMT
GMTGreenwich Mean Time格林尼治标准时间是一种以地球自转为基础的时间计量系统。它 将地球自转一周的时间间隔等分为24小时以此确定计时标准UTCUniversal Time Coordinated协调世界时是一种以原子钟为基础的时间计量系统。它规定 铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当 原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时UTC会执行闰秒来保证其计时与 地球自转的协调一致
时间戳转换
C语言的time.h模块提供了时间获取和时间戳转换的相关函数可以方便地进行秒计数器、日期时 间和字符串之间的转换 time.h
#include stdio.h
#include stdlib.h
#include time.h
// struct tm {
// int tm_sec; /* 秒范围从0 到59 */
// int tm_min; /* 分范围从0 到59 */
// int tm_hour; /* 小时范围从0 到23 */
// int tm_mday; /* 一月中的第几天范围从1 到31 */
// int tm_mon; /* 月范围从0 到11 */
// int tm_year; /* 自1900 年起的年数 */
// int tm_wday; /* 一周中的第几天范围从0 到6 */
// int tm_yday; /* 一年中的第几天范围从0 到365 */
// int tm_isdst; /* 夏令时 */
// };
int main(int argc, char const *argv[])
{time_t Time; // 时间戳struct tm Date;// Time time(NULL);time(Time);printf(Time %ld\n, Time); // Time 1695695610Date *localtime(Time);printf(%d\n, Date.tm_year 1900);printf(%d\n, Date.tm_mon 1);printf(%d\n, Date.tm_mday);printf(%d\n, Date.tm_hour);printf(%d\n, Date.tm_min);printf(%d\n, Date.tm_sec);printf(%d\n, Date.tm_wday);printf(%d\n, Date.tm_yday);printf(%s\n, ctime(Time));printf(%s\n, asctime(Time));puts(asctime(Date));char t[50];strftime(t, 50, %H-%M-\%S, Date);printf(t);return 0;
}
BKP检测
BKPBackup Registers备份寄存器BKP可用于存储用户应用程序数据。当VDD2.03.6V电源被切断他们仍然由VBAT1.8 3.6V维持供电。当系统在待机模式下被唤醒或系统复位或电源复位时他们也不会被复位TAMPER引脚产生的侵入事件将所有备份寄存器内容清除RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲存储RTC时钟校准寄存器用户数据存储容量20字节中容量和小容量/ 84字节大容量和互联型
BKP基本结构 TRC简介
RTCReal Time Clock实时时钟 RTC是一个独立的定时器可为系统提供时钟和日历的功能 RTC和时钟配置系统处于后备区域系统复位时数据不清零VDD2.03.6V断电后可借助VBAT 1.8 3.6V供电继续走时 32位的可编程计数器可对应Unix时间戳的秒计数器 20位的可编程预分频器可适配不同频率的输入时钟 可选择三种RTC时钟源
LSI振荡器时钟40KHzHSE时钟除以128通常为8MHz/128LSE振荡器时钟通常为32.768KHzRTC框图 RTC基本结构 RTC操作注意事项
执行以下操作将使能对BKP和RTC的访问 设置RCC_APB1ENR的PWREN和BKPEN使能PWR和BKP时钟 设置PWR_CR的DBP使能对BKP和RTC的访问
若在读取RTC寄存器时RTC的APB1接口曾经处于禁止状态则软件首先必须等待RTC_CRL寄存器 中的RSF位寄存器同步标志被硬件置1 必须设置RTC_CRL寄存器中的CNF位使RTC进入配置模式后才能写入RTC_PRL、RTC_CNT、 RTC_ALR寄存器 对RTC任何寄存器的写操作都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的 RTOFF状态位判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时才可以写入RTC寄存器
11.1 读写备份寄存器BKP 电源 常用函数
void BKP_DeInit(void); // 将所有配置清0
// 用于Tamper侵入检测功能
void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel); // 配置有效电平高电平、低电平触发
void BKP_TamperPinCmd(FunctionalState NewState); // 是否开启
void BKP_ITConfig(FunctionalState NewState); // 配置中断
void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource); // 时钟输出功能的配置
void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue); // 设置RTC校准值
// 读写BKP
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);
FlagStatus BKP_GetFlagStatus(void);
void BKP_ClearFlag(void);
ITStatus BKP_GetITStatus(void);
void BKP_ClearITPendingBit(void);
void PWR_DeInit(void);
void PWR_BackupAccessCmd(FunctionalState NewState); //备份寄存器访问使能
void PWR_PVDCmd(FunctionalState NewState);
void PWR_PVDLevelConfig(uint32_t PWR_PVDLevel);
void PWR_WakeUpPinCmd(FunctionalState NewState);
void PWR_EnterSTOPMode(uint32_t PWR_Regulator, uint8_t PWR_STOPEntry);
void PWR_EnterSTANDBYMode(void);
FlagStatus PWR_GetFlagStatus(uint32_t PWR_FLAG);
void PWR_ClearFlag(uint32_t PWR_FLAG);
代码 main.c
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include Key.h
uint16_t Write[] {0x1234, 0x5678};
uint16_t Read[2];
uint8_t KeyNum;
int main(void)
{OLED_Init();KEY_Init();OLED_ShowString(1, 1, W:);OLED_ShowString(2, 1, R:);// BKP初始化RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);// 备份寄存器访问使能PWR_BackupAccessCmd(ENABLE);while (1){KeyNum Key_GetNum();if (KeyNum 1){BKP_WriteBackupRegister(BKP_DR1, Write[0]); // DR 1-10 c8t6BKP_WriteBackupRegister(BKP_DR2, Write[1]);Write[0];Write[1];OLED_ShowHexNum(1, 3, Write[0], 4);OLED_ShowHexNum(2, 3, Write[0], 4);}Read[0] BKP_ReadBackupRegister(BKP_DR1);Read[1] BKP_ReadBackupRegister(BKP_DR2);OLED_ShowHexNum(1, 3, Read[0], 4);OLED_ShowHexNum(2, 3, Read[1], 4);}
}
11.2 RTC实时时钟
常用函数
void RCC_LSEConfig(uint8_t RCC_LSE); // 启动LSE时钟
void RCC_LSICmd(FunctionalState NewState); // 配置LSI内部低速时钟
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource); // CLK时钟源数据选择器
void RCC_RTCCLKCmd(FunctionalState NewState); // 启动RCCCLK
void RCC_GetClocksFreq(RCC_ClocksTypeDef *RCC_Clocks);
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalStateNewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalStateNewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalStateNewState);
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG); // 获取标志位
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState); // RTC中断
void RTC_EnterConfigMode(void); // 进入配置模式
RTC_CRL_CNF 1 void RTC_ExitConfigMode(void); // 退出
uint32_t RTC_GetCounter(void); // 获取计数器的值
void RTC_SetCounter(uint32_t CounterValue); // 写入计数器的值设置时间
void RTC_SetPrescaler(uint32_t PrescalerValue); // PSC
void RTC_SetAlarm(uint32_t AlarmValue); // 闹钟
uint32_t RTC_GetDivider(void); // 余数寄存器
void RTC_WaitForLastTask(void); // 等待上一次操作完成
void RTC_WaitForSynchro(void); // 等待同步
FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);
void RTC_ClearFlag(uint16_t RTC_FLAG);
ITStatus RTC_GetITStatus(uint16_t RTC_IT);
void RTC_ClearITPendingBit(uint16_t RTC_IT);
代码 main.c
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include RTC.h
int main(void)
{OLED_Init();MyRTC_Init();OLED_ShowString(1, 1, Date:XXXX-XX-XX);OLED_ShowString(2, 1, Time:XX:XX:XX);OLED_ShowString(3, 1, CNT :);OLED_ShowString(4, 1, DIV :);while (1){MyRTC_ReadTime();OLED_ShowNum(1, 6, MyRTC_Time[0], 4);OLED_ShowNum(1, 11, MyRTC_Time[1], 2);OLED_ShowNum(1, 14, MyRTC_Time[2], 2);OLED_ShowNum(2, 6, MyRTC_Time[3], 2);OLED_ShowNum(2, 9, MyRTC_Time[4], 2);OLED_ShowNum(2, 12, MyRTC_Time[5], 2);OLED_ShowNum(3, 6, RTC_GetCounter(), 10);OLED_ShowNum(4, 6, RTC_GetDivider(), 10);// 1s 1000ms 这样计数就变成0-999 999为1s// OLED_ShowNum(4, 6, (32767 - RTC_GetDivider()) / 32767.0 * 999,10);}
}
RTC.c
#include stm32f10x.h // Device header
#include time.h
uint32_t MyRTC_Time[] {2023, 9, 28, 10, 25, 59};
void MyRTC_SetTime(void);
void MyRTC_Init(void)
{// 1.初始化BKPRCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);PWR_BackupAccessCmd(ENABLE);// 判断主电源断开还是备用电源断开防止主电源断开也重置计数器if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5){// 2.开启LSE时钟并等待开启完成RCC_LSEConfig(RCC_LSE_ON);while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) ! SET);// 3.选择RCCCLK时钟源RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);RCC_RTCCLKCmd(ENABLE);// 4.等待同步写入完成防止时钟不同步出现时钟bug 不写影响也不大RTC_WaitForSynchro();RTC_WaitForLastTask();// 5.配置PSC 对PRL寄存器写操作函数已经实现进入配置模式和退出配置模式不需要手写RTC_SetPrescaler(32768 - 1); // LSE时钟频率32.768KHz 32768Hz进行32768分频 1HzRTC_WaitForLastTask(); // 写操作都需要等待一下// 6.设置初始时间MyRTC_SetTime();// 向BKPDR1写入设置的值备用电源断了则清空DR1BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);}// 备用电源没有掉则只需要同步一下即可else{RTC_WaitForSynchro();RTC_WaitForLastTask();}
}
void MyRTC_SetTime(void)
{time_t time_cnt; // 时间戳struct tm time_date;time_date.tm_year MyRTC_Time[0] - 1900;time_date.tm_mon MyRTC_Time[1] - 1;time_date.tm_mday MyRTC_Time[2];time_date.tm_hour MyRTC_Time[3];time_date.tm_min MyRTC_Time[4];time_date.tm_sec MyRTC_Time[5];time_cnt mktime(time_date) - 8 * 60 * 60;RTC_SetCounter(time_cnt);RTC_WaitForLastTask();
}
void MyRTC_ReadTime(void)
{time_t time_cnt;struct tm time_date;time_cnt RTC_GetCounter() 8 * 60 * 60;time_date *localtime(time_cnt);MyRTC_Time[0] time_date.tm_year 1900;MyRTC_Time[1] time_date.tm_mon 1;MyRTC_Time[2] time_date.tm_mday;MyRTC_Time[3] time_date.tm_hour;MyRTC_Time[4] time_date.tm_min;MyRTC_Time[5] time_date.tm_sec;
}
十二、PWR
12.1 PWR简介
PWRPower Control电源控制 PWR负责管理STM32内部的电源供电部分可以实现可编程电压监测器和低功耗模式的功能 可编程电压监测器PVD可以监控VDD电源电压当VDD下降到PVD阀值以下或上升到PVD阀值 之上时PVD会触发中断用于执行紧急关闭任务 低功耗模式包括睡眠模式Sleep、停机模式Stop和待机模式Standby可在系统空闲 时降低STM32的功耗延长设备使用时间
电源框图 上电复位和掉电复位 可编程电压监测器 低功耗模式 模式选择 睡眠模式
执行完WFI/WFE指令后STM32进入睡眠模式程序暂停运行唤醒后程序从暂停的地方继续运行 SLEEPONEXIT位决定STM32执行完WFI或WFE后是立刻进入睡眠还是等STM32从最低优先级的 中断处理程序中退出时进入睡眠 在睡眠模式下所有的I/O引脚都保持它们在运行模式时的状态 WFI指令进入睡眠模式可被任意一个NVIC响应的中断唤醒 WFE指令进入睡眠模式可被唤醒事件唤醒
停机模式 执行完WFI/WFE指令后STM32进入停止模式程序暂停运行唤醒后程序从暂停的地方继续运行 1.8V供电区域的所有时钟都被停止PLL、HSI和HSE被禁止SRAM和寄存器内容被保留下来 在停止模式下所有的I/O引脚都保持它们在运行模式时的状态 当一个中断或唤醒事件导致退出停止模式时HSI被选为系统时钟 当电压调节器处于低功耗模式下系统从停止模式退出时会有一段额外的启动延时 WFI指令进入停止模式可被任意一个EXTI中断唤醒 WFE指令进入停止模式可被任意一个EXTI事件唤醒
待机模式 执行完WFI/WFE指令后STM32进入待机模式唤醒后程序从头开始运行 整个1.8V供电区域被断电PLL、HSI和HSE也被断电SRAM和寄存器内容丢失只有备份的寄存 器和待机电路维持供电 在待机模式下所有的I/O引脚变为高阻态浮空输入 WKUP引脚的上升沿、RTC闹钟事件的上升沿、NRST引脚上外部复位、IWDG复位退出待机模式
12.2 修改主频 代码
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
int main(void)
{OLED_Init();OLED_ShowString(1, 1, SYSCLK:);OLED_ShowNum(1, 8, SystemCoreClock, 8);while (1){OLED_ShowString(2, 1, Running...);Delay_ms(500);OLED_ShowString(2, 1, );Delay_ms(500);}
}
12.3 数据收发睡眠模式
代码
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include Serial.h
#include LED.h
uint8_t RxData;
uint8_t Pin_9, Pin_10;
int main(void)
{OLED_Init();Serial_Init();OLED_ShowString(1, 1, RxData:);while (1){if (Serial_GetRxFlag() 1){RxData Serial_GetRxData();Serial_SendByte(RxData);OLED_ShowHexNum(1, 8, RxData, 2);}// 没有数据要发送但代码一直执行所以可以采用睡眠模式OLED_ShowString(2, 1, Running...);Delay_ms(500);OLED_ShowString(2, 1, );Delay_ms(500);__WFI(); // 中断唤醒}
}
12.4 停止模式
常用函数
void PWR_DeInit(void);
void PWR_BackupAccessCmd(FunctionalState NewState); // 使能后备区域的访问
// 配置PVD使能电压
void PWR_PVDCmd(FunctionalState NewState);
void PWR_PVDLevelConfig(uint32_t PWR_PVDLevel);
// 使能WKUP引脚唤醒功能在待机模式下使用
void PWR_WakeUpPinCmd(FunctionalState NewState);
// 进入停止模式
void PWR_EnterSTOPMode(uint32_t PWR_Regulator, uint8_t PWR_STOPEntry);
// 进入待机模式
void PWR_EnterSTANDBYMode(void);
FlagStatus PWR_GetFlagStatus(uint32_t PWR_FLAG);
void PWR_ClearFlag(uint32_t PWR_FLAG);
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include CountSensor.h //红外对射式传感器
int main(void)
{OLED_Init();CountSensor_Init();OLED_ShowString(1, 1, Count:);while (1){OLED_ShowNum(1, 7, CountSensor_Get(), 5);OLED_ShowString(2, 1, Running...);Delay_ms(500);OLED_ShowString(2, 1, );Delay_ms(500);PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI); // 进入停止模式中断唤醒SystemInit(); // 进入停止模式之后主频被修改为HSI时钟8MHz 需要恢复一下系统时钟}
}
12.5 待机模式
实时时钟待机
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include RTC.h
int main(void)
{OLED_Init();MyRTC_Init();RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); // 保证RTC在待机模式下能够正常执行不那么依赖MyRTCOLED_ShowString(1, 1, CNT:);OLED_ShowString(2, 1, ALR:);OLED_ShowString(3, 1, ALRF:);PWR_WakeUpPinCmd(ENABLE); // WKUP接高电平也可以唤醒程序uint32_t Alarm RTC_GetCounter() 10;RTC_SetAlarm(Alarm);OLED_ShowNum(2, 6, Alarm, 10);while (1){OLED_ShowNum(1, 6, RTC_GetCounter(), 10);OLED_ShowNum(3, 6, RTC_GetFlagStatus(RTC_FLAG_ALR), 1); // 闹钟标志位OLED_ShowString(4, 1, Running...);Delay_ms(500);OLED_ShowString(4, 1, );Delay_ms(500);OLED_ShowString(4, 9, STANDBY...);Delay_ms(500);OLED_ShowString(4, 1, );Delay_ms(500);// 一般用于极度省电模式把能关的外设都关闭OLED_Clear();PWR_EnterSTANDBYMode(); // 代码从头开始运行会自动调用System_Init()}
}
十三、看门狗WDG
13.1 WDG简介
WDGWatchdog看门狗 看门狗可以监控程序的运行状态当程序因为设计漏洞、硬件故障、电磁干扰等原因出现卡死或 跑飞现象时看门狗能及时复位程序避免程序陷入长时间的罢工状态保证系统的可靠性和安全 性看门狗本质上是一个定时器当指定时间范围内程序没有执行喂狗重置计数器操作时看门 狗硬件电路就自动产生复位信号 STM32内置两个看门狗 独立看门狗IWDG独立工作对时间精度要求较低 窗口看门狗WWDG要求看门狗在精确计时窗口起作用
IWDG框图: IWDG键寄存器:
键寄存器本质上是控制寄存器用于控制硬件电路的工作 在可能存在干扰的情况下一般通过在整个键寄存器写入特定值来代替控制寄存器写入一位的功 能以降低硬件电路受到干扰的概率 IWDG超时时间:
13.2 窗口看门狗WWDG WWDG工作特性:
递减计数器T[6:0]的值小于0x40时WWDG产生复位 递减计数器T[6:0]在窗口W[6:0]外被重新装载时WWDG产生复位 递减计数器T[6:0]等于0x40时可以产生早期唤醒中断EWI用于重装载计数器以避免WWDG复 位定期写入WWDG_CR寄存器喂狗以避免WWDG复位 WWDG超时时间: 超时时间 TWWDG TPCLK1 × 4096 × WDGTB预分频系数× (T[5:0] 1) 窗口时间 TWIN TPCLK1 × 4096 × WDGTB预分频系数× (T[5:0] - W[5:0]) 其中TPCLK1 1 / FPCLK1 IWDG和WWDG对比:
看门狗一旦启用就无法关闭防止误操作烧入代码的时候需要按着复位键再松开
13.3 实现IWDG
常用函数
void IWDG_WriteAccessCmd(uint16_t IWDG_WriteAccess); // 写使能控制
void IWDG_SetPrescaler(uint8_t IWDG_Prescaler); // 写预分频器PR
void IWDG_SetReload(uint16_t Reload); // 写重装值RLR
void IWDG_ReloadCounter(void); // 重新装载寄存器0xAAAA 喂狗
void IWDG_Enable(void); // 启用IWDG0xCCCC
FlagStatus IWDG_GetFlagStatus(uint16_t IWDG_FLAG);
// 查看看门狗标志位
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);
void RCC_ClearFlag(void);
代码 修改oled
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include Key.h
int main(void)
{OLED_Init();OLED_ShowString(1, 1, IWDG TEST);if (RCC_GetFlagStatus(RCC_FLAG_IWDGRST) SET){OLED_ShowString(2, 1, IWDGRST);Delay_ms(500);OLED_ShowString(2, 1, );Delay_ms(100);RCC_ClearFlag();}else{OLED_ShowString(2, 1, RST);Delay_ms(500);OLED_ShowString(2, 1, );Delay_ms(100);}// 设置看门狗IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);// 1000msIWDG_SetPrescaler(IWDG_Prescaler_16);IWDG_SetReload(2499);// 开启前先喂一次狗IWDG_ReloadCounter();IWDG_Enable();while (1){Key_GetNum(); // 模拟卡死一直按着按键超时IWDG_ReloadCounter(); // 主循环需要一直喂狗OLED_ShowString(4, 1, FEED); // 表示feedDelay_ms(200);OLED_ShowString(4, 1, );Delay_ms(200);}
}
13.4 实现WWDG
常用函数
void WWDG_DeInit(void);
void WWDG_SetPrescaler(uint32_t WWDG_Prescaler);
void WWDG_SetWindowValue(uint8_t WindowValue);
void WWDG_EnableIT(void);
void WWDG_SetCounter(uint8_t Counter);
void WWDG_Enable(uint8_t Counter);
FlagStatus WWDG_GetFlagStatus(void);
void WWDG_ClearFlag(void);
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include Key.h
int main(void)
{OLED_Init();OLED_ShowString(1, 1, IWDG TEST);if (RCC_GetFlagStatus(RCC_FLAG_WWDGRST) SET){OLED_ShowString(2, 1, WWDGRST);Delay_ms(500);OLED_ShowString(2, 1, );Delay_ms(100);RCC_ClearFlag();}else{OLED_ShowString(2, 1, RST);Delay_ms(500);OLED_ShowString(2, 1, );Delay_ms(100);}// 设置窗口看门狗RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);WWDG_SetPrescaler(WWDG_Prescaler_8);WWDG_SetWindowValue(0x40 | 21); // 30msWWDG_Enable(0x40 | 54);//|0x40是设置T6 50mswhile (1){Key_GetNum(); // 模拟卡死一直按着按键超时// WWDG_SetCounter(0x40 | 54); //主循环需要一直喂狗喂狗太早// OLED_ShowString(4,1,FEED); //表示feed// Delay_ms(20);// OLED_ShowString(4,1, );// Delay_ms(20);Delay_ms(20);WWDG_SetCounter(0x40 | 54); // 放在40ms延时之后}
}