🚀write in front🚀
🔎大家好,我是黄桃罐头,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝💬本系列哔哩哔哩江科大STM32的视频为主以及自己的总结梳理📚
🚀Projeet source code🚀
💾工程代码放在了本人的Gitee仓库:iPickCan (iPickCan) - Gitee.com
引用:
STM32入门教程-2023版 细致讲解 中文字幕_哔哩哔哩_bilibili
Keil5 MDK版 下载与安装教程(STM32单片机编程软件)_mdk528-CSDN博客
STM32之Keil5 MDK的安装与下载_keil5下载程序到单片机stm32-CSDN博客
0. 江协科技/江科大-STM32入门教程-各章节详细笔记-查阅传送门-STM32标准库开发_江协科技stm32笔记-CSDN博客
【STM32】江科大STM32学习笔记汇总(已完结)_stm32江科大笔记-CSDN博客
江科大STM32学习笔记(上)_stm32博客-CSDN博客
STM32学习笔记一(基于标准库学习)_电平输出推免-CSDN博客
STM32 MCU学习资源-CSDN博客
stm32学习笔记-作者: Vera工程师养成记
stem32江科大自学笔记-CSDN博客
术语:
英文缩写 | 描述 |
GPIO:General Purpose Input Onuput | 通用输入输出 |
AFIO:Alternate Function Input Output | 复用输入输出 |
AO:Analog Output | 模拟输出 |
DO:Digital Output | 数字输出 |
内部时钟源 CK_INT:Clock Internal | 内部时钟源 |
外部时钟源 ETR:External clock | 时钟源 External clock |
外部时钟源 ETR:External clock mode 1 | 外部时钟源 Extern Input pin 时钟模式1 |
外部时钟源 ETR:External clock mode 2 | 外部时钟源 Extern Trigger 时钟模式2 |
外部时钟源 ITRx:Internal trigger inputs | 外部时钟源,ITRx (Internal trigger inputs)内部触发输入 |
外部时钟源 TIx:external input pin | 外部时钟源 TIx (external input pin)外部输入引脚 |
CCR:Capture/Comapre Register | 捕获/比较寄存器 |
OC:Output Compare | 输出比较 |
IC:Input Capture | 输入捕获 |
TI1FP1:TI1 Filter Polarity 1 | Extern Input 1 Filter Polarity 1,外部输入1滤波极性1 |
TI1FP2:TI1 Filter Polarity 2 | Extern Input 1 Filter Polarity 2,外部输入1滤波极性2 |
正文:
0. 概述
从 2024/06/12 定下计划开始学习下江协科技STM32课程,接下来将会按照哔站上江协科技STM32的教学视频来学习入门STM32 开发,本文是视频教程 P2 STM32简介一讲的笔记。
定时器共四个部分,分为八个小节笔记。本小节为第一部分第一节。
🌳在第一部分,是定时器的基本定时的功能:定时中断功能、内外时钟源选择
🌳在第二部分,是定时器的输出比较功能,最常见的用途是产生PWM波形,用于驱动电机等设备
🌳在第三部分,是定时器的输入捕获功能和主从触发模式,来实现测量方波频率
🌳在第四部分,是定时器的编码器接口,能够更加方便读取正交编码器的输出波形,编码电机测速
1.🚢第一个代码:AD单通道
接线图:
电位器的内部结构是这样的:
左边和右边的两个引脚接的是电阻的两个固定端,中间这个引脚接的是滑动抽头。电位器外边这里有个十字形状的槽,可以拧,往左拧抽头就往左靠,往右拧,抽头就往右靠。所以外围电路这里,我们把左边的固定端接在负极,右边的固定端接在正极,中间就可以输出,从负极到正极可调的电压了,把可调的电压输出接在PA0。
2.🚢ADC初始化步骤
AD的初始化看这个结构图
ADC初始化的步骤具体的步骤:
🦄第一步,开启RCC时钟,包括ADC和GPIO的时钟。另外这里ADC CLK的分频器也需要配置一下。
🦄第二步,配置GPIO,把需要用的GPIO配置成模拟输入的模式。
🦄第三步,配置多路开关
把左边的通道接入到右边的规则组列表里。这个过程就是我们之前说的点菜,把各个通道的菜列在菜单里。
🦄第四步,配置ADC转换器,在库函数里是用结构体来配置的,可以配置这一大块电路的参数。
如果需要模拟看门狗,会有几个函数用来配置阈值和监测通道的。
如果想开启中断,就在中断输出控制里用ITconfig函数开启对应的中断输出,然后再在NVIC里配置一下优先级,这样就能触发中断了。
不过模拟看门狗中断我们本节暂时不用。
第五步,开关控制,调用一下ADC_Cmd的函数开启ADC。
这样ADC就配置完成了就能正常工作了。
第六步,校准
当然在开启ADC之后,根据手册里的建议,我们还可以对ADC进行一下校准,这样可以减小误差。在ADC工作的时候,如果想要软件触发转换,会有函数可以触发。如果想读取转换结果,也会有函数,可以读取结果。这个等会介绍扩函数的时候就可以看到了。
3.🚢ADC相关的库函数
首先我们看一下ADC CLK的配置函数,打开这个rcc.h文件,拖到最后。
这个函数是用来配置ADC CLK分频器的。它可以对APB2的72MHz时钟选择二、四、六、八分频,输入到ADC CLK,这就是这个函数的作用。
然后我们找一下ADC的库函数,打开adc.h文件,拖到最后。
三个初始化相关函数
这三个函数和其它模块的库函数一样,都是老朋友,不用多讲了。
ADC_Cmd
这个是用于给ADC上电的,也就是这里的开关控制
ADC_DMACmd
这个是用于开启DMA输出信号的。如果使用DMA转运数据,就得调用这个函数。这个我们下节讲DMA的时候再用。
ADC_ITConfig
中断输出控制,也就是这里用于控制某个中断能不能通过NVIC
四个校准相关函数
接下来这里有四个函数
分别是复位校准、获取复位校准状态、开始校准、获取开始校准状态,这就是用于控制校准的函数。我们在ADC初始化完成之后依次调用就行了。
ADC_SoftwareStartConvCmd
ADC软件开始转换控制,这个就是用于软件触发的函数了,调用一下就能软件触发转换了,也就是这里的触发控制,我们目前使用软件触发。
ADC_GetSoftwareStartConvStatus
ADC获取软件开始转换状态,从名字上来看,这个函数好像是判断转换是不是正在进行的。我们是不是可以调用这个函数来判断转换是否已经结束?答案是不行的。这个函数就是用来获取CR2的SWSTART这一位。
在手册里可以看到这一位的作用是开始转换规则通道,由软件设置该位以启动转换,转换开始后硬件马上清除此位。
因此,ADC_SoftwareStartConvCmd这个函数就是给SWSTART位置1,以开始转换的。
而ADC_GetSoftwareStartConvStatus这个函数是返回SWSTART的状态。
由于SWSTART位在转换开始后立刻清零了。所以这个函数的返回值跟转换是否结束毫无关系。
那如何才能知道转换是否结束?
我们需要用到下面这个函数:
ADC_GetFlagStatus
获取标志位状态,然后参数给EOC的标志位,判断EOC标志位是不是置1了。如果转换结束,EOC标志位置1,然后调用这函数判断标志位。这样才是正确的判断转换是否结束的方法。
所以ADC_GetSoftwareStartConvStatus这个函数其实没啥用,我们一般不用,不要被它误导了。
然后下面这两个函数是用来配置间断模式的。
第一个函数是每隔几个通道间断一次。第二个函数是是不是启用间断模式。需要间断模式的话,可以了解一下。
ADC_RegularChannelConfig
DC规则组通道配置,这个函数比较重要。它的作用就是给序列的每个位置填写指定的通道,就是填写点菜菜单的过程
第一个参数是ADCx,第二个ADC channel就是理想指定的通道。第三个rank就是序列几的位置。然后第四个sample time就是指定通道的采样时间。
ADC_ExternalTrigConvCmd
ADC外部触发转换控制,就是是否允许外部触发转换。
ADC_GetConversionValue
ADC获取转换值,这个函数也比较重要,就是获取AD转换的数据寄存器,读取转换结果,就要使用这个函数。
ADC_GetDualModeConversionValue
之后,ADC获取双模式转换值,这个是双ADC模式读取转换结果的函数,我们暂时不用。
以上这些函数就是对ADC的一些基本功能和规则组的配置。
九个配置ADC注入组的函数
然后接下来这里有一大批函数,里面都带了一个injected,就是注入组的意思。
这一大批函数都是对ADC注入组进行配置的。
三个模拟看门狗配置的函数
然后下面的这三个函数就是对模拟看门狗进行配置的。
第一个是是否启动模拟看门狗。第二个是配置高低阈值。第三个是配置看门的通道。
ADC_TempSensorVrefintCmd
ADC温度传感器内部参考电压控制,这个是用来开启内部的两个通道的。如果你要用这两个通道,得调用一下这个函数开启一下,要不然是读不到正确的结果的。
四个获取或清除标志位函数
分别是获取标志位状态、清除标志位、获取中断状态、清除中断挂起位,这些函数也是常用函数了,不用多说。
看完这些函数我们来开始写代码。
4.🚢代码实现
//配置ADC是工作在独立模式,还是双ADC模式
//独立模式,ADC1和ADC2各转换各的
#define ADC_Mode_Independent ((uint32_t)0x00000000)
//其余的都是双ADC模式,相对比较复杂
#define ADC_Mode_RegInjecSimult ((uint32_t)0x00010000)
#define ADC_Mode_RegSimult_AlterTrig ((uint32_t)0x00020000)
#define ADC_Mode_InjecSimult_FastInterl ((uint32_t)0x00030000)
#define ADC_Mode_InjecSimult_SlowInterl ((uint32_t)0x00040000)
#define ADC_Mode_InjecSimult ((uint32_t)0x00050000)
#define ADC_Mode_RegSimult ((uint32_t)0x00060000)
#define ADC_Mode_FastInterl ((uint32_t)0x00070000)
#define ADC_Mode_SlowInterl ((uint32_t)0x00080000)
#define ADC_Mode_AlterTrig ((uint32_t)0x00090000)
ADC校准,如下四个函数对应ADC校准的四个步骤
//1.复位校准
void ADC_ResetCalibration(ADC_TypeDef* ADCx);
//2.等地复位校准
FlagStatus ADC_GetResetCalibrationStatus(ADC_TypeDef* ADCx);
//3.开始校准
void ADC_StartCalibration(ADC_TypeDef* ADCx);
//4.等待校准完成
FlagStatus ADC_GetCalibrationStatus(ADC_TypeDef* ADCx);
4.1 实验1-单ADC转换
代码 AD.C
#include "stm32f10x.h" // Device header
#include "AD.h"void AD_Init(void)
{//开启RCC时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //APB2 GPIOA时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //APB2 GPIOA时钟//RCC_ADC Clock预分频值RCC_ADCCLKConfig(RCC_PCLK2_Div6); //RCC_ADC_Clock=72Mhz/6=12Mhz//GPIOA_Pin0配置为模拟输入//GPIOA_Pin0作为ADC1的输入通道1GPIO_InitTypeDef gpioInitStructure;gpioInitStructure.GPIO_Mode = GPIO_Mode_AIN;gpioInitStructure.GPIO_Pin = GPIO_Pin_0;gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &gpioInitStructure);//ADC模拟输入多路开关选择ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);//ADC_Init初始化ADC_InitTypeDef ADC_InitStruct;ADC_StructInit(&ADC_InitStruct);ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //非外部触发,软件触发方式ADC_InitStruct.ADC_ScanConvMode = DISABLE; //ADC使用非扫描模式ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; //ADC使用非连续转换模式(单次模式)ADC_InitStruct.ADC_NbrOfChannel = 1; //ADC规则组转换列表里的数目ADC_Init(ADC1, &ADC_InitStruct);//ADC开关ADC_Cmd(ADC1, ENABLE);//ADC校准ADC_ResetCalibration(ADC1); //软件置标志位while(ADC_GetResetCalibrationStatus(ADC1) == SET); //当校准完成之后,硬件自动清除标志位ADC_StartCalibration(ADC1); //软件置标志位while(ADC_GetCalibrationStatus(ADC1) == SET); //当校准完成之后,硬件自动清除标志位
}uint16_t GetValue(void)
{uint16_t ADCValue;//软件ADC触发转换ADC_SoftwareStartConvCmd(ADC1, ENABLE);//等待ADC完成while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);ADCValue = ADC_GetConversionValue(ADC1); //读取清除EOC标志位return ADCValue;
}
代码AD.h
#ifndef __AD_H__
#define __AD_H__void AD_Init(void);
uint16_t GetValue(void);#endif
代码 main.c
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"extern uint16_t Num;int main(int argc, char *argv[])
{ uint16_t ADValue;OLED_Init();OLED_ShowString(1, 1, "Cnt:");AD_Init();OLED_ShowString(1, 1, "ADValue:");while(1){ADValue = GetValue();OLED_ShowNum(2, 1, ADValue, 6);Delay_ms(100);}return 1;
}
实验结果:
如果想显示一下实际的电压值怎么办?
这只需要对这个数据进行一个线性变换就行了。我们在上面定义一个变量表示电压
然后我们将转换的结果再进行一下运算Voltage = (float)ADValue / 4095 * 3.3;,这样就能得到电压值。
另外这里要注意因为AD value是整数,在除4095之后会舍弃掉小数部分,这样会导致计算错误。所以我们先把AD value类型强转为float,这样再除才不会出问题。
由于目前我们的OLED驱动还没有显示浮点数的函数(这个之后讲OLED的时候再加)。目前这里我们如果想显示浮点数,可以用显示整数的函数来操作。
如果直接用显示整数的函数的话,小数就会舍弃掉,所以我们要用两个显示整数的函数,将第二个显示整数的函数的值再进行一下处理变成小数显示出来,也就是先把书扩大100倍,比如原来是1.23,现在就是123,然后再对100取余,就是23,这样就把1.23的小数部分取出来了。
另外由于浮点数是不能取余的,所以(Voltage * 100)要括起来,然后再进行强制类型转换变成整数,再对它取余。这样就可以显示浮点数了。
这里实际上AD值等于4096时才对应3.3V负,会有一个数的偏差,所以AD值最大的4095实际上对应的应该是比3.3V小一丢丢,没有办法达到满量程3.3V,这个是受限于ADC的结构,具体就不再细说了。总之就是认为4095对应3.3V伏可以,认为,4096对应3.3V也可以,只有一点点偏差,也看不出来差别。
如果就只是进行阈值判断数据记录的话,也可以不进行变换,直接使用原始的AD数据,这样也是可以
连续转换模式
目前我们使用的是第一种转换方式:单次转换、非扫描。
我们还可以使用第二种转换方式:连续转换、非扫描。这个模式的好处就是不需要不断的触发,也不需要等待转换完成的。
这种模式只要对程序稍作修改就行。我们要切换为连续转换模式,那么这个参数就要改成enable。
连续转换仅需要在最开始触发一次就行了,所以这里软件触发转换的函数就可以挪到初始化的最后,即在初始化完成之后,触发一次就行了。
所以在这里就不需要判断标志位这行代码了,
直接return数据寄存器的值就行了。这样程序就是单通道连续转换非扫描的模式。
下载程序现象和刚才是一样的,也能实现单通道的AD转换。这就是连续转换非车描的模式。
4.2 实验2,多通道ADC
接线图:
在这里我们使用了四个AD通道,第一个通道还是电位器,接在PA0口。之后上面又接了三个传感器模块,分别是光敏传感器,热敏传感器、反射式红外传感器,它们的vcc和gnd都分别接在面包板的正负极。然后这个AO就是模拟量的输出引脚,三个模块的AO分别接在PA1,PA2和PA3口,加上电位器的PA0,总共是四个输入通道,同样这些GPIO口也是可以在PA0到PB1之间任意选择的。这里就选择前四个。
如何实现多通道采集?
我们首先想到的应该是后面这两种扫描模式
利用这个列表把四个通道都填进去,然后触发转换,这样就能实现多通道了。
这样确实是一种不错的方法,但是有个数据覆盖的问题。
如果想要用扫描模式实现多通道,最好要配合DMA来实现。我们下节讲完DMA之后,再来试一下扫描模式。
那我们一个通道转换完成之后,手动把数据转运出来不就行了吗?为啥非要用DMA来转运?
这个方案看似简单,但是实际操作起来会有一些问题。
- 第一个问题就是在扫描模式下,启动列表之后,它里面每一个单独的通道转换完成之后,不会产生任何的标志位,也不会触发中断。你不知道某一个通道是不是转换完了。它只有在整个列表都转换完成之后,才会产生一次EOC标志位,才能触发中断。而这时前面的数据就已经覆盖丢失了。
- 第二个问题就是AD转换是非常快的,刚才我们也计算过转换一个通道,大概只有几微秒。也就是说,如果你不能在几微秒的时间内把数据转运走,数据就会丢失,这对我们程序手动转移数据要求就比较高了。
所以在扫描模式下,手动转移数据是比较困难的。不过比较困难,也不是说手动转运不可行,我们可以使用间断模式,在扫描的时候,每转换一个通道就暂停一次,等我们手动把数据转运走之后再继续触发,继续下一次转换。这样可以实现手动转移数据的功能。
但是由于单个通道转换完成之后,没有标志位。所以启动转换完成之后,只能通过Delay延时的方式,延时足够长的时间,才能保证转换完成,这种方式既不能让我们省心,也不能提高效率。所以我们暂时不推荐使用。
这些方法都不行,我们本节是不是就不能实现多通道了?答案是能实现,而且非常简单,怎么实现?
我们可以使用单次转换、非扫描的模式来实现多通道。只需要在每次触发转换之前,手动更改一下列表第一个位置的通道就行了。
比如,第一次转换,先写入通道0,之后触发,等待、读值。第二次转换,再把通道0,改成通道1,之后触发,等待、读值。第三次转换,再先改成通道二修改......这样在转换前先指定一下通道,再启动转换,就可以轻松的实现多通道转换的功能了。
那么我们本次的代码就比较简单,只需要做一些简单的修改就行了。
我们可以把这个填充通道的这一句代码剪切,
然后放到触发转换之前
然后我们想指定的通道,可以作为成AD_GetValue函数的参数
这样就行了。
这样我们在调用AD_GetValue进行转换时,只需要指定一个转换的通道,返回值就是我们指定通道的结果了。
接下来我们现在要指定的通道是通道0/1/2/3,所以上面这里的GPIO初始化也不要忘了加上这几个引脚。
源码如下:
AD.c
#include "stm32f10x.h" // Device header
#include "AD.h"void AD_Init(void)
{//开启RCC时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //APB2 GPIOA时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //APB2 GPIOA时钟//RCC_ADC Clock预分频值RCC_ADCCLKConfig(RCC_PCLK2_Div6); //RCC_ADC_Clock=72Mhz/6=12Mhz//GPIOA_Pin0配置为模拟输入//GPIOA_Pin0作为ADC1的输入通道1GPIO_InitTypeDef gpioInitStructure;gpioInitStructure.GPIO_Mode = GPIO_Mode_AIN;gpioInitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &gpioInitStructure);//ADC_Init初始化ADC_InitTypeDef ADC_InitStruct;ADC_StructInit(&ADC_InitStruct);ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //非外部触发,软件触发方式ADC_InitStruct.ADC_ScanConvMode = DISABLE; //ADC使用非扫描模式ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; //ADC使用非连续转换模式(单次模式)//ADC_InitStruct.ADC_ContinuousConvMode = ENABLE; //ADC使用非连续转换模式(单次模式)ADC_InitStruct.ADC_NbrOfChannel = 1; //ADC规则组转换列表里的数目ADC_Init(ADC1, &ADC_InitStruct);//ADC开关ADC_Cmd(ADC1, ENABLE);//ADC校准ADC_ResetCalibration(ADC1); //软件置标志位while(ADC_GetResetCalibrationStatus(ADC1) == SET); //当校准完成之后,硬件自动清除标志位ADC_StartCalibration(ADC1); //软件置标志位while(ADC_GetCalibrationStatus(ADC1) == SET); //当校准完成之后,硬件自动清除标志位}uint16_t GetValue(uint8_t ADC_Channel)
{uint16_t ADCValue;//ADC模拟输入多路开关选择ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);//软件ADC触发转换ADC_SoftwareStartConvCmd(ADC1, ENABLE);//等待ADC完成while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);ADCValue = ADC_GetConversionValue(ADC1); //读取清除EOC标志位return ADCValue;
}
AD.h
#ifndef __AD_H__
#define __AD_H__void AD_Init(void);
uint16_t GetValue(uint8_t ADC_Channel);#endif
main.c
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"extern uint16_t Num;int main(int argc, char *argv[])
{ uint16_t ADValue;float voltage;OLED_Init();OLED_ShowString(1, 1, "Cnt:");AD_Init();OLED_ShowString(1, 1, "AD1:0000");OLED_ShowString(2, 1, "AD2:0000");OLED_ShowString(3, 1, "AD3:0000");OLED_ShowString(4, 1, "AD3:0000");while(1){ADValue = GetValue(ADC_Channel_0);OLED_ShowNum(1,5,ADValue,4);ADValue = GetValue(ADC_Channel_1);OLED_ShowNum(2,5,ADValue,4);ADValue = GetValue(ADC_Channel_2);OLED_ShowNum(3,5,ADValue,4);ADValue = GetValue(ADC_Channel_3);OLED_ShowNum(4,5,ADValue,4);Delay_ms(100);}return 1;
}
实验结果: