STM32学习笔记

2-2点灯

新建工程文件的步骤:

  1. 建立工程文件夹,Keil中新建工程,选择型号
  2. 工程文件夹里建立Start、Library、User等文件夹,复制固件库里面的对应文件到工程文件夹
  3. 工程里面(Keil里面设置)对应建立Start、Library、User等同名称的分组,然后将文件夹内的文件添加到工程分组。
  4. 工程选项(魔法棒)中,C/C++,Include Paths内声明所有包含头文件的文件夹
  5. 工程选项(魔法棒)中,C/C++,Define内定义USE_STDPERIPH_DRIVER,这样在编译中头文件才生效
  6. 工程选项(魔法棒)中,Debug,下拉列表选择对应调试器,Settings,Flash Download里勾选Reset and run

库函数的操作:

对于一个GPIO的执行程序,以下过程是必不可少的:

  1. 设置时钟:在时钟控制寄存器(RCC_CR)进行时钟设置
  2. GPIO口的初始化:包括选择GPIO的输出模式,选择GPIO端口,选择GPIO端口速度,这三个变量构建GPIO初始化的结构体;
  3. 具体程序执行(以LED亮灯为例):设置端口输出为高低电平。

3-1GPIO(通用输入输出口)输出

  1. GPIO的一般知识

    1. 可配置8种输入输出模式
    2. 引脚电平:0-3.3V,部分引脚可容忍5V
    3. 输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器,模拟通信协议输出时序等
    4. 输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等
  2. GPIO模式

    1. 模式名称 性质 特征
      浮空输入 数字输入 可读取引脚电平,若引脚悬空,则电平不确定
      上拉输入 数字输入 可读取引脚电平,内部连接上拉电阻,悬空时默认高电平
      下拉输入 数字输入 可读取引脚电平,内部连接下拉电阻,悬空时默认低电平
      模拟输入 模拟输入 GPIO无效,引脚直接接入内部ADC
      开漏输出 数字输出 可输出引脚电平,高电平为高阻态,低电平接VSS
      推挽输出 数字输出 可输出引脚电平,高电平接VDD,低电平接VSS
      复用开漏输出 数字输出 由片上外设控制,高电平为高阻态,低电平接VSS
      复用推挽输出 数字输出 由片上外设控制,高电平接VDD,低电平接VSS
    2. 解释:

      1. 施密特触发器的作用:消除电平抖动,给寄存器稳定的电平输入
      2. 上拉输入描述的“内部连接上拉电阻”是指

      在这里插入图片描述

      上面的开关闭合,下面的开关关闭,下拉输入模式相反

      1. 开漏输出中的“高电平为高阻态”指的是P-MOS无效,开漏输出只输出低电平(只有低电平有输出能力)

      2. 推挽输出状态,P-MOS和N-MOS都有效,高低电平都可以输出(高低电平都有输出能力)

        在这里插入图片描述

3-2LED闪烁,LED流水灯,蜂鸣器(GPIO输出的应用)

上述三个程序是对GPIO输入输出的练习。

首先,都是要对GPIO口时钟进行设置(GPIO由APB2寄存器管理)

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);

然后是GPIO口的初始化

GPIO_Init(GPIOB,&GPIO_InitStructrue);

包含了期望初始化的GPIO_x(x=A,B,C),还有一个结构体GPIO_InitStructrue的地址,包括了GPIO口的输入输出模式,PIN口选择,GPIO输入输出速度

然后在while循环中执行期望的程序

while (1)
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_12,Bit_RESET);
	Delay_ms(500);
	GPIO_WriteBit(GPIOB,GPIO_Pin_12,Bit_SET);
	Delay_ms(500);
}

关键:头函数把所有函数都包含在内,转到定义就可以看到函数参数是如何选择的,以及参数的数据类型。在文件《STM32F103xx固件函数库用户手册》中包含了函数的用法,不会可以查阅

3-3 GPIO输入

  1. C语言数据类型

    1. char,int,long long,float,double的位数,以及stdint的关键字(typedefine重定义获得)

      关键字 位数 表示范围 stdint关键字 ST关键字
      char 8 -128 ~ 127 int8_t s8
      unsigned char 8 0 ~ 255 uint8_t u8
      short 16 -32768 ~ 32767 int16_t s16
      unsigned short 16 0 ~ 65535 uint16_t u16
      int 32 -2147483648 ~ 2147483647 int32_t s32
      unsigned int 32 0 ~ 4294967295 uint32_t u32
      long 32 -2147483648 ~ 2147483647
      unsigned long 32 0 ~ 4294967295
      long long 64 -(2^64)/2 ~ (2^64)/2-1 int64_t
      unsigned long long 64 0 ~ (2^64)-1 uint64_t
      float 32 -3.4e38 ~ 3.4e38
      double 64 -1.7e308 ~ 1.7e308
  2. C语言中的一些高级用法

    1. 这些用法包含两个步骤:变量的定义和变量的引用

    2. 宏定义#define

      1. 用途:用字符代替数字,提高辨识度
      2. 定义宏定义#define ABC 12345
      3. 引用宏定义int a = ABC; //等效于int a = 12345;
    3. 类型定义typedef

      1. 将一个比较长的变量类型名换个名字,便于使用定义typedef(只能对变量类型进行操作)
      2. 定义typedef: typedef unsigned char uint8_t;
      3. 引用typedef: uint8_t a; //等效于unsigned char a;
    4. 结构体struct

      1. 相比于数组是将相同数据类型的数据进行打包,结构体是将不同数据类型的数据进行打包

      2. 定义结构体变量: struct{char x; int y; float z;} StructName;如果这个结构体的定义太长,而且这个结构体经常被用到,可以用typedef将这个结构体重新命名

      3. 引用结构体成员:

        StructName.x = ‘A’;

        StructName.y = 66;

        StructName.z = 1.23;

    5. 枚举enum

      1. 定义一个取值受限制的整型变量,用于限制变量取值范围(只能是整形)

      2. 定义枚举变量: enum{FALSE = 0, TRUE = 1} EnumName;(同理,如果这个枚举的定义太长,而且这个枚举经常被用到,可以用typedef将这个枚举重新命名)

      3. 引用枚举成员:

        EnumName = FALSE;

        EnumName = TRUE;

3-4 GPIO输入的应用

  1. 当外设数量增多的时候,使用模块化的程序设计
  2. 这一节主要还是GPIO输入输出的应用,首先要对GPIO初始化才能进行下面的操作

5-1 EXIT外部中断

  1. **中断:**在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行

  2. **中断优先级:**当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源

  3. **中断嵌套:**当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回

  4. 中断流程图

    在这里插入图片描述

  5. NVIC:嵌套中断向量控制器:统一分配中断优先级与管理中断。一个中断线路(EXIT,TIM,ADC等)可以安排16位优先级(因为NVIC由一个16位寄存器控制)。

    1. NVIC基本结构:多个中断线路输入,优先级最高的中断程序输出

      在这里插入图片描述

    2. NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级**(对16位优先级进行分组)**,下面是5种分组方式

      分组方式 抢占优先级 响应优先级
      分组0 0位,取值为0 4位,取值为0~15
      分组1 1位,取值为0~1 3位,取值为0~7
      分组2 2位,取值为0~3 2位,取值为0~3
      分组3 3位,取值为0~7 1位,取值为0~1
      分组4 4位,取值为0~15 0位,取值为0
    3. 抢占优先级和响应优先级:抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队

  6. EXTI(Extern Interrupt)外部中断:外部中断可以监测GPIO口的电平信号,当其指定的GPIO口产生电平变化,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后可中断CPU主程序,使CPU执行EXIT对应的中断程序。

    1. EXIT触发方式:
      1. 上升沿(低电平到高电平瞬间触发)
      2. 下降沿(高电平到低电平瞬间触发)
      3. 双边沿/软件触发(低/高电平到高/低电平瞬间触发)
    2. 支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断(PA1,PB1,PC1只能选一个作为中断引脚)
    3. 通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒
    4. 触发响应方式:中断响应(申请中断)/事件响应(中断响应不转向CPU,而是转向其他外设,用来触发其他外设的操作)
  7. EXTI的基本结构

    在这里插入图片描述

    1. AFIO(数据选择器):用于进行引脚选择(因为相同的Pin不能同时触发中断——PA1,PB1,PC1只能选一个作为中断引脚)
    2. EXTI边沿检测及控制:检测引脚是否存在中断触发
    3. 在“EXTI边沿检测及控制——NVIC”中看到两个:“EXTI9_5,EXTI15_10”这两个将引脚9_5,引脚15_10分别触发相同的中断函数,但是中断函数中需要增加标志位来区分到底是哪个引脚的中断进入程序
  8. EXTI程序配置基本思路(参照EXTI基本结构)

    1. 配置RCC——将GPIO和AFIO的时钟信号打开(EXTI默认打开,不用设置,NVIC是内核外设,默认打开)
    2. 初始化GPIO——将GPIO模式设置为输入模式(在参考手册GPIO那一章,写了每一个外设对应的GPIO配置,可以找到当GPIO引脚设置为EXTI输入线时,GPIO的输入配置为浮空输入或带上拉输入或带下拉输入:8.1.11 外设的GPIO配置
    3. 配置AFIO——选择合适的GPIO_Pin
    4. 配置EXTI——选择边沿触发方式(上升沿/下降沿/双边沿),选择触发响应方式(中断响应/事件响应)
    5. 配置NVIC——
      1. 选择抢占优先级和响应优先级的分组(这个步骤一个工程只需要执行一次)
      2. 将这个Pin引脚选择合适的输入通道(EXTI0,EXTI1,…,EXTI9_5,EXTI15_10),并为对应的中断程序选择一个合适的优先级(1.选择是抢占优先级还是响应优先级;2.选择优先级的次序)
      3. 找到对应引脚的中断函数(在启动文件里,找到的是函数的句柄,这个句柄的地址是固定的,因为当中断触发时,硬件只会到固定的地址寻找中断函数,但是我们编写的中断函数会被编译器自由配置地址,因此要配置中断函数,我们首先要在启动文件找到对应输入引脚到NVIC的中断函数句柄:EXTI15_10_IRQHandler,然后在这个函数名下编写中断函数,编译器会自动寻找函数的地址)
      4. 注意,配置中断函数时,对于EXTI15_10,EXTI9_5这两条线的中断函数句柄EXTI5_10_IRQHandler,EXTI9_5_IRQHandler中要进行中断标志位的判断(以GPIO_Pin_14为例):EXTI_GetITStatus(EXTI_Line14) == SET表示EXTI_Line_14正在申请中断;要多写几个If判断标志位。其他的只管一条EXTI线的中断函数中,也要写这个中断位的判断(函数统一);在函数结束后,需要清除中断标志位(EXTI_GetFlagStatus(EXTI_Line14)):EXTI_ClearFlag(EXTI_Line14),防止程序一直进入中断,进入死循环

6-1 TIM定时中断

  1. 定时器的类型

    类型 编号 总线 功能
    高级定时器 TIM1、TIM8 APB2 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能
    通用定时器 TIM2、TIM3、TIM4、TIM5 APB1 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能
    基本定时器 TIM6、TIM7 APB1 拥有定时中断、主模式触发DAC的功能

    对于F103C8T6,只有4个寄存器:TIM1、TIM2、TIM3、TIM4(一个高级寄存器,3个通用寄存器)

  2. 基本定时器

    1. 结构

      在这里插入图片描述

    2. 各个器件的作用:

      1. PSC预分频器(16位):基本定时器一般直接接内部时钟,频率为72MHz,这个数CNT计数器装不下(只有16位),因此利用PSC预分频器将72MHz分频到CNT计数器能够接受的范围
      2. CNT计数器(16位):对时钟信号进行计数,当来一个上升沿/下降沿,计数器加一(向上计数,还有向下计数)
      3. 自动重装载寄存器:记录当计数器到达什么值后,计数器清零,并发出中断信号
      4. 另外,PSC预分频器和自动重装载寄存器都存在一个影子寄存器,真正工作的是影子寄存器。以预分频器为例,当预分频器的值更新,影子寄存器还会继续工作,直到影子寄存器对应的计数周期结束后,预分频器上新的值才会存入影子寄存器。对于自动重装载寄存器,如果新的值(100)小于原来的值(200),此时计数器已经到达(150),那么新的值也会等待计数周期技术后产生更新事件才会更新——有影子寄存器的情况,当没有影子寄存器时,计数器会持续计数到16位的最大值
    3. 定时中断:基本定时器连接内部时钟,内部时钟的频率经过预分频器进行分频,计数器进行技术,自动重装寄存器含有重装信息(超过多少就触发中断,并且将计数器清零)

    4. 主模式触发DAC功能 :如果DAC要求每隔一段时间触发一次,正常的思路是利用定时器设置中断,在中断程序触发DAC输出(缺点:使得主程序出与频繁被中断的状态,降低CPU效率) ;利用定时器的主模式触发DAC:将定时器本身的定时中断映射到触发输出TRGO的位置,将TRGO接到DAC的触发转换引脚上,这样就不是通过中断触发DAC(不通过CPU,不占用CPU资源)

  3. 通用定时器

    1. 内外时钟源选择(相比基本定时器,通用定时器能够选择不同的时钟源):
    2. ETR(外部时钟信号):经过极性选择,边沿检测和预分频器,输入滤波就可以进入触发控制器,给时基单元提供时钟信号**(外部时钟模式2)——最常用
      2. TRGI(trigger in ):触发输入作为外部时钟(外部时钟模式1)。这个模式的TRGI由1.ETR引脚信号;2.ITR:其他定时器的输出信号(定时器的级联)经过数据选择器的信号;3. TI1F_ED:由CH1外部引脚输入获得的上升下降沿脉冲信号;
    3. TI1FP1(CH1引脚的时钟)与TI2FP2(CH2引脚的时钟)
  4. 定时中断的基本结构

    在这里插入图片描述

    左边的是定时器的时钟源选择;中间的运行控制模块,是控制寄存器的一些位(选择启动停止,向上计数/向下计数等)

  5. 一些频率的计算

    1. 计数器技术频率(每秒计数器记多少次):CK_CNT = CK_PSC / (PSC + 1)(时钟信号源频率/(预分频器设置数+1))

    2. 计数器溢出频率(每秒钟会达到多少次计数器溢出):CK_CNT_OV = CK_CNT / (ARR + 1)

      ​ = CK_PSC / (PSC + 1) / (ARR + 1)(时钟信号源频率/(预分频器设置数+1)/(自动重装器设置数+1))

    3. 自动重装器+1是因为是从0开始计数的

  6. RCC时钟树

    时钟源的生成和分配简图如下所示

    ng&pos_id=img-G3k5GHn7-1740971844506)

6-2 定时中断程序设计

简化的定时中断的基本结构如下图所示

在这里插入图片描述

设计定时器中断程序的基本步骤如下(设计的逻辑就是将上面的中断流程图打通)

  1. RCC开启时钟(也就打开了定时器的基准时钟和整个外设的工作时钟)

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);

  2. 选择时基单元的时钟源:RCC,外部时钟模式2(ETR),外部时钟模式1(ETR外部时钟,ITRx其他定时器,TIxGPIO捕获通道)

    void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
    void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
    void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,
                                    uint16_t TIM_ICPolarity, uint16_t ICFilter);
    void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
                                 uint16_t ExtTRGFilter);
    void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, 
                                 uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
    void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
                       uint16_t ExtTRGFilter);
    
  3. 配置时基单元:用结构体配置PSC预分频器,ARR自动重装器,CNT计数器

    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitSturcture;
    	TIM_TimeBaseInitSturcture.TIM_ClockDivision = TIM_CKD_DIV1;//决定对内部时钟的分频情况
    	TIM_TimeBaseInitSturcture.TIM_CounterMode = TIM_CounterMode_Up;
    	TIM_TimeBaseInitSturcture.TIM_Period = 5 - 1;
    	TIM_TimeBaseInitSturcture.TIM_Prescaler = 1;
    	TIM_TimeBaseInitSturcture.TIM_RepetitionCounter = 0;
    	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitSturcture);
    
  4. 配置输出中断控制,允许更新中断输出到NVIC

    TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);

  5. 配置NVIC,在NVIC打开定时器中断的通道,并分配一个优先级

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    	NVIC_InitTypeDef NVIC_InitStructure;
    	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
    	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    	NVIC_Init(&NVIC_InitStructure);
    
  6. 使能运行控制模块,计数器开始计数

    TIM_Cmd(TIM2,ENABLE);

  7. 编写定时器中断函数(注意判断标志位和清楚标志位)

    void TIM2_IRQHandler(void)
    {
    	if (TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
    	{
    		TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
    	}
    	
    }
    

6-3 TIM输出比较

  1. OC(Output Compare)输出比较:输出比较可以通过比较CNT与CCR(capture/compare register)寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形

    1. 每个高级定时器和通用定时器都拥有4个输出比较通道(可以输出4个不同的输出比较结果)
    2. 高级定时器的前3个通道额外拥有死区生成和互补输出的功能
  2. PWM(Pulse Width Modulation)脉冲宽度调制

    1. 针对对象:具有惯性的系统(LED,电机)

    2. 原理:通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量

      在这里插入图片描述

    3. 参数

      1. 频率 = 1 / TS

      2. 占空比 = TON / TS

      3. 分辨率 = 占空比变化步距(占空比可以变化的最小步长)
        在这里插入图片描述

  3. 输出比较模式

    模式 描述
    冻结 CNT=CCR时,REF保持为原状态
    匹配时置有效电平 CNT=CCR时,REF置有效电平
    匹配时置无效电平 CNT=CCR时,REF置无效电平
    匹配时电平翻转 CNT=CCR时,REF电平翻转
    强制为无效电平 CNT与CCR无效,REF强制为无效电平
    强制为有效电平 CNT与CCR无效,REF强制为有效电平
    PWM模式1 向上计数:CNT<CCR时,REF置有效电平,CNT≥CCR时,REF置无效电平
    向下计数:CNT>CCR时,REF置无效电平,CNT≤CCR时,REF置有效电平
    PWM模式2 向上计数:CNT<CCR时,REF置无效电平,CNT≥CCR时,REF置有效电平
    向下计数:CNT>CCR时,REF置有效电平,CNT≤CCR时,REF置无效电平

一般常用的是PWM模式1和PWM模式2

  1. PWM基本结构

    在这里插入图片描述

    可以看到,PWM首先需要定时器的时基单元中的CNT计数器正常工作,与CCR进行比较,对输出比较单元进行模式设置之后才能输出PWM波形

    1. 从波形图可以看出PWM的参数计算
      1. PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
        1. PWM频率=计数器的中断频率=时钟源频率/预分频器数+1/自动重装器+1
      2. PWM占空比: Duty = CCR / (ARR + 1)
      3. PWM分辨率: Reso = 1 / (ARR + 1)

6-4 TIM输出比较编程——产生PWM波形

  1. 基于PWM基本结构图进行编程安排

    1. 开始APB1时钟(通用定时器都在APB1总线上),开启APB2时钟(产生的PWM波形由GPIO口输出,这个GPIO口通过通用定时器的电路图可以看出名称是TIM2_CH1(如果用的是第一个输出比较单元),从stm32引脚定义图可以找到TIM2_CH1对应的是PA0端口,这种GPIO端口的使用方式是端口复用功能)

      RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
      
    2. 初始化时基单元(设置PSC预分频器,ARR自动重装器)

      TIM_InternalClockConfig(TIM2);//选择内部时钟RCC为时基时钟
      	
      	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitSturcture;
      	TIM_TimeBaseInitSturcture.TIM_ClockDivision = TIM_CKD_DIV1;//决定对内部时钟的分频情况
      	TIM_TimeBaseInitSturcture.TIM_CounterMode = TIM_CounterMode_Up;
      	TIM_TimeBaseInitSturcture.TIM_Period = 100 - 1;//ARR
      	TIM_TimeBaseInitSturcture.TIM_Prescaler = 720-1;//PSC
      	TIM_TimeBaseInitSturcture.TIM_RepetitionCounter = 0;
      	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitSturcture);
      
    3. 初始化输出比较单元(设置CCR捕获/比较寄存器,使能,输出比较模式(这个笔记上面有写),极性选择)

      TIM_OCInitTypeDef TIM_OCInitStructure;
      	TIM_OCStructInit(&TIM_OCInitStructure);//给结构体设置初值
      	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
      	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
      	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
      	TIM_OCInitStructure.TIM_Pulse = 0x32;//CCR
      	TIM_OC3Init(TIM2,&TIM_OCInitStructure);
      
    4. 使能时基单元

      TIM_Cmd(TIM2,ENABLE);
      
    5. 初始化PWM输出端口(初始化GPIO,注意,GPIO的模式是复用推免输出模式)

      	GPIO_InitTypeDef GPIOStructure;
      	GPIOStructure.GPIO_Mode = GPIO_Mode_AF_PP;//推挽输出模式
      	GPIOStructure.GPIO_Pin = GPIO_Pin_2;
      	GPIOStructure.GPIO_Speed = GPIO_Speed_50MHz;//这个速度是GPIO的输出速度,当输入模式时这个参数无用
      	GPIO_Init(GPIOA,&GPIOStructure);
      

6-5 TIM输入捕获(Input Capture)

  1. 输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前CNT(时计单元的计数器)的值将被锁存到CCR(输出比较单元的捕获/比较器)中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数。

    1. 每个高级定时器和通用定时器都拥有4个输入捕获通道
    2. 可配置为PWMI(PWM Input)模式,同时测量频率和占空比
    3. 可配合主从触发模式(主模式,从模式,触发源选择),实现硬件全自动测量
  2. 频率测量法

    1. 测频法:在固定时间T内,对上升沿计次得到N,频率为f=N/T
    2. 测周法:在两个上升沿内,以标准频率fc计次,得到两个上升沿内经过的标准频率次数N,频率为fx=fc/N

    在这里插入图片描述

  3. 输入捕获硬电路

    1. 捕获通道详细电路

      在这里插入图片描述

      1. fdts:滤波器的采样时钟
      2. 滤波器的配置由TIMx_CCMR1寄存器的ICF位进行设置
      3. 边沿检测其可以选择上升沿检测还是下降沿检测
      4. TIMx_CCER寄存器的CC1P位配置极性
      5. 经过数据选择器后得到触发信号TI1FP1
      6. 经过分频器(TIMx_CCMR1的ICPS位)与输出使能(TIMxCCER的CC1E),那么输入端产生指定边沿信号,经过层层电路,到达IC1PS,就可以让此时的CNT的值转运到CCR。
      7. 注意:每捕获一次CNT的值,CNT会自动清零,重新计数。清零方式:利用从模式控制器(输入信号是触发信号TI1FP1)——主从触发模式(相关信号可查看手册)
        1. 主模式:将定时器内部的信号,映射到TRGO引脚,用于触发别的外设
        2. 从模式:接受其他外设或者自身外设的一些信号,用于控制自身定时器的运行,也就是被别的信号控制。TRGI出发的从模式程序可以被自动运行,也就是程序自动化的来源,能够减轻CPU负担
      8. 触发源选择:选择从模式的触发信号源(从模式的一部分)
    2. 注意:从通用定时器电路以及捕获详细电路可以看出,一个输出捕获单元可以接受两个输出捕获单元的触发信号,基于这两个信号可以配置PWMI模式(检测PWM的周期与占空比)

    3. 输入捕获基本结构(测量输入信号频率)

      在这里插入图片描述

      原理(测周法测量频率):时基单元开启,时间单元中的基本时钟信号开始工作,CNT计数器开始工作,当GPIO检测到上升沿输入,CCR1=CNT,CNT=0(利用触发信号TI1FP1经过从模式使CNT清零),连续工作的示意图如左上角所示。此时输入信号的频率=72MHz/(PSC+1)/CCR1(N=CCR1,fc=72MHz/(PSC+1))

    4. PWMI基本结构

      在这里插入图片描述

      相比输入捕获结构,多了一个触发信号TI1FP2,当TI1FP1检测上升沿,TI1FP2检测下降沿,那么TI1FP2触发信号就能够获得高电平的计数值。CCR1/CCR2=占空比。

6-6 TIM输入捕获程序设计

基于输入捕获基本结构可以得到输入捕获初始化流程

  1. 定时器与GPIO口的时钟打开

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启GPIO时钟
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//开启TIM2时钟
    
  2. 配置输入GPIO口

    	//输入GPIO口的配置
    	GPIO_InitTypeDef GPIO_InitStructure;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA,&GPIO_InitStructure);
    
  3. 时基单元的配置

    	//时基单元的配置
    	TIM_InternalClockConfig(TIM3);
    	TIM_TimeBaseInitTypeDef TIM_TimBaseInitStructure;
    	TIM_TimBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    	TIM_TimBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    	TIM_TimBaseInitStructure.TIM_Period = 65536-1;//ARR,用65536满量程计数
    	TIM_TimBaseInitStructure.TIM_Prescaler = 72-1;//RSC
    	TIM_TimBaseInitStructure.TIM_RepetitionCounter = 0;
    	TIM_TimeBaseInit(TIM3,&TIM_TimBaseInitStructure);
    
  4. 输入捕获单元的配置

    	//输入捕获单元的配置
    	TIM_ICInitTypeDef TIM_ICInitStrcture;
    	TIM_ICInitStrcture.TIM_Channel = TIM_Channel_1;
    	TIM_ICInitStrcture.TIM_ICFilter = 0xF;
    	TIM_ICInitStrcture.TIM_ICPolarity = TIM_ICPolarity_Rising;
    	TIM_ICInitStrcture.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    	TIM_ICInitStrcture.TIM_ICSelection = TIM_ICSelection_DirectTI;
    	TIM_ICInit(TIM3,&TIM_ICInitStrcture);
    
  5. 从模式触发的配置

    	//从模式触发的配置
    	TIM_SelectInputTrigger(TIM3,TIM_TS_TI1FP1);//从模式触发源选择
    	TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);//设置从模式,对CNT计数器自动操作
    
  6. 使能定时器(给定时器上电)

    TIM_Cmd(TIM3,ENABLE);
    

6-7 TIM编码器结构测速

  1. 编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度(本质:测周法测速,相同时间内CNT增长到的位置可以表示为速度的大小

  2. 每个高级定时器和通用定时器都拥有1个编码器接口

  3. 两个输入引脚借用了输入捕获的通道1和通道2(定时器包含了4个输入捕获引脚和一个编码器接口)

  4. 编码器接口基本结构

    在这里插入图片描述

  5. 编码器如何通过正交信号判断正反转

    当在A相计数,若A相上升沿,此时B相低电平,那么就是正传;若B相高电平就是反转。

    基于上述原理有下面的编码器工作模式
    在这里插入图片描述

    令TI1FP1为A相,TI2FP2为B相,那么,从表中读出,当在TI1和TI2上计数时,TI1FP1上升沿,此时TI2FP2为高电平,那么编码器向下计数(反转)。

7-1 ADC(Analog-Digital Converter)数模转换器

  1. ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁。(STM32寄存器只能处理01跳变的信号)

  2. 原理:12位逐次逼近型ADC,1us转换时间。将模拟电压信号数值转为二进制信号(12位)存储到指定寄存器中。

  3. 输入电压范围:0-3.3V,转换结果范围:0~4095(12位最大4095)

  4. 18个输入通道,可测量16个外部和2个内部信号源

  5. 规则组和注入组两个转换单元

  6. 模拟看门狗自动监测输入电压范围

  7. ADC框图

    从电路图可以看出,ADC输入信号从GPIO口输入,选择注入通道/规则通道,基于二分法进行逐次逼近,得到一组二进制值存入对应寄存器,基于看门狗或者其他中断规则进行中断输出。

    1. ADC基本结构

      在这里插入图片描述

    2. ADC输入通道与GPIO引脚对应关系(可由引脚定义图获得)

    3. 转换模式:ADC存在4种转换模式

      1. 单次转换,非扫描模式

        在这里插入图片描述

      2. 连续转换,非扫描模式

        在这里插入图片描述

      3. 单次转换,扫描模式

        在这里插入图片描述

      4. 连续转换,扫描模式

        在这里插入图片描述

    4. 触发控制(触发进行ADC转换)

      在这里插入图片描述

    5. 数据对齐(12位的ADC数据存入16位的寄存器)
      在这里插入图片描述

      一般选择右对齐

    6. 校准

      在这里插入图片描述

7-2 AD单通道&AD多通道

基于ADC基本结构图可以得到初始化基本流程

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);//ADC时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//GPIO时钟
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);//ADC分频时钟
	
	GPIO_InitTypeDef GPIO_InitStrcture;
	GPIO_InitStrcture.GPIO_Mode = GPIO_Mode_AIN;//ADC模式下,GPIO默认设置成为AIN模式
	GPIO_InitStrcture.GPIO_Pin = GPIO_Pin_0 |GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
	GPIO_InitStrcture.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStrcture);
	
	
		
	ADC_InitTypeDef ADC_InitStructure;//ADC转换器配置
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_ExternalTrigConv =  ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_NbrOfChannel = 1;
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;
	ADC_Init(ADC1,&ADC_InitStructure);
	
	ADC_Cmd(ADC1,ENABLE);//ADC上电
	
	ADC_ResetCalibration(ADC1);//调用复位校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);//等待复位校准完成(寄存器标志位Reset)
	ADC_StartCalibration(ADC1);//启动校准
	while (ADC_GetCalibrationStatus(ADC1) == SET);//等待校准完成
	

8-1 DMA直接存储器存取

  1. DMA(Direct Memory Access)直接存储器存取

  2. DMA可以提供外设(外设寄存器)和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源

  3. 每个通道都支持软件触发和特定的硬件触发

  4. DMA基本结构

    在这里插入图片描述

    注意:

    1. Flash是只读寄存器,不可写入
    2. DMA进行数据转运时,需要进行结构体信息初始化,填写外设的起始地址,数据宽度,地址是否自增(是在同一个寄存器进行搬运还是搬运完一个寄存器就去下一个寄存器进行搬运);存储器的地址是否自增选项是指搬运来的数据是否按顺序寄存器地址进行存储,一般按照顺序存储,这样不会覆盖数据。
    3. 传输计数器:存储转运次数(自减计数器)
    4. 自动重装器:当传输计数器自减到0,判断是是否要自动恢复到最初的值。
    5. DMA的触发:由M2M(Memory to Memory)模块决定由硬件触发还是软件触发
    6. 软件触发希望尽快把传输计数器清零,因此自动重装器与软件出发不能同时用,会使得DMA一直处于工作状态
    7. 软件触发一般是存储器到存储器的转运,硬件触发一般是外设寄存器到存储器的转运。
  5. DMA请求

    在这里插入图片描述

    每个通道都可以进行软件触发,但是每个通道只可以进行部分硬件触发请求

  6. DMA的数据宽度与对齐

    在这里插入图片描述

    当数据源地址宽度与目标地址宽度对不上时,如果目标宽度长了,在前面补零,如果目标宽度短了,舍弃源地址宽度前面的部分。

8-2 DMA数据转运

  1. DMA软件触发转运数据(从存储器到存储器数据的转运)

    软件触发时,DMA的自动重装器DISABLE,,当计数器自减到0,转运结束。

    初始化程序

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);//打开时钟
    	
    	DMA_InitTypeDef DMA_InitStrcture;
    	DMA_InitStrcture.DMA_PeripheralBaseAddr = AddrA;//外设的起始地址
    	DMA_InitStrcture.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设的数据宽度
    	DMA_InitStrcture.DMA_PeripheralInc = DMA_PeripheralInc_Enable;//外设的地址是否自增选项
    	DMA_InitStrcture.DMA_MemoryBaseAddr = AddrB;//存储器的起始地址
    	DMA_InitStrcture.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器的数据宽度
    	DMA_InitStrcture.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器的地址是否自增选项
    	DMA_InitStrcture.DMA_BufferSize = Size;//传输计数器大小为Size(函数自动传入)
    	DMA_InitStrcture.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向
    	DMA_InitStrcture.DMA_M2M = DMA_M2M_Enable;//选择软件触发还是硬件触发
    	DMA_InitStrcture.DMA_Mode = DMA_Mode_Normal;//是否选择自动重装器
    	DMA_InitStrcture.DMA_Priority = DMA_Priority_Medium;//DMA优先级
    	DMA_Init(DMA1_Channel1,&DMA_InitStrcture);
    	
    	DMA_Cmd(DMA1_Channel1,DISABLE);
    

    为了方便设置新的计数器的值,另外设置了函数。值得注意的是,DMA的计数器设置要求DMA处于断电状态,因此要求DMA先断电,修改计数器的值,再上电。

    	DMA_Cmd(DMA1_Channel1,DISABLE);
    	DMA_SetCurrDataCounter(DMA1_Channel1,Num_Size);
    	DMA_Cmd(DMA1_Channel1,ENABLE);
    	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//等待DMA转运结束
    	DMA_ClearFlag(DMA1_FLAG_TC1);//标志位需要手动清除
    
  2. DMA硬件触发数据转运

    基于ADC的连续扫描模式进行DMA数据转运

    在这里插入图片描述

    初始化

    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);//ADC时钟
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//GPIO时钟
    	RCC_ADCCLKConfig(RCC_PCLK2_Div6);//ADC分频时钟
    	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);//DMA时钟
    	
    	GPIO_InitTypeDef GPIO_InitStrcture;
    	GPIO_InitStrcture.GPIO_Mode = GPIO_Mode_AIN;
    	GPIO_InitStrcture.GPIO_Pin = GPIO_Pin_0 |GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
    	GPIO_InitStrcture.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA,&GPIO_InitStrcture);
    	
    	//ADC转换器选择规则组,有4个通道需要转运,下面时进行转运顺序的排序	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5);
    		
    	ADC_InitTypeDef ADC_InitStructure;
    	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//连续模式
    	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据右对齐
    	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//外部触发选择
    	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    	ADC_InitStructure.ADC_NbrOfChannel = 4;//通道数目
    	ADC_InitStructure.ADC_ScanConvMode = ENABLE;//扫描/非扫描模式
    	ADC_Init(ADC1,&ADC_InitStructure);
    	
    	DMA_InitTypeDef DMA_InitStrcture;
    	DMA_InitStrcture.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//外设的起始地址
    	DMA_InitStrcture.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设的数据宽度
    	DMA_InitStrcture.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设的地址是否自增选项
    	DMA_InitStrcture.DMA_MemoryBaseAddr = (uint32_t)AD_value;//存储器的起始地址
    	DMA_InitStrcture.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//存储器的数据宽度
    	DMA_InitStrcture.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器的地址是否自增选项
    	DMA_InitStrcture.DMA_BufferSize = 4;//传输计数器大小
    	DMA_InitStrcture.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向
    	DMA_InitStrcture.DMA_M2M = DMA_M2M_Disable;//选择软件触发还是硬件触发
    	DMA_InitStrcture.DMA_Mode = DMA_Mode_Circular;//是否选择自动重装器
    	DMA_InitStrcture.DMA_Priority = DMA_Priority_Medium;//DMA优先级
    	DMA_Init(DMA1_Channel1,&DMA_InitStrcture);
    	
    	DMA_Cmd(DMA1_Channel1,ENABLE);//DMA上电,此时DMA是否工作取决于ADC的硬件信号(下面ADC_DMACmd函数连接了ADC和DMA,当ADC->DR寄存器存入新的值,DMA就进行转运)
    	
    	ADC_DMACmd(ADC1,ENABLE);
    	
    	ADC_Cmd(ADC1,ENABLE);
    	
    	ADC_ResetCalibration(ADC1);//调用复位校准
    	while (ADC_GetResetCalibrationStatus(ADC1) == SET);//等待复位校准完成(寄存器标志位Reset)
    	ADC_StartCalibration(ADC1);//启动校准
    	while (ADC_GetCalibrationStatus(ADC1) == SET);//等待校准完成
    	
    	ADC_SoftwareStartConvCmd(ADC1,ENABLE);//软件触发进行转换,因为ADC处于连续,扫描模式,DMA的自动重装器也处于循环状态,因此当ADC接受了软件触发,那么ADC的转运就会一直进行下去,在主函数直接调取目标寄存器下的值就可获得ADC的转运结果。
    

9-1 USART串口协议

  1. 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
  2. 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发

9-2 USART串口外设

  1. USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器

  2. 同步收发器:在通信时需要时钟信号(CLK)来同步数据的发送和接收。发送端和接收端使用同一个时钟信号,确保数据在时钟的上升沿或下降沿被采样。(SPI和I2S使用的就是同步收发器)

  3. 异步收发器:异步收发器不需要时钟信号,依靠预定义的波特率来同步数据。发送端和接收端使用相同的波特率,通过起始位和停止位来标识数据帧的开始和结束。(UART和USART)

  4. USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里(USART是一个硬件电路,可以将寄存器的字节数据封装为数据帧,并从TX引脚发送出去,也可以自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里

  5. 自带波特率发生器,最高达4.5Mbits/s(系统时钟72MHz的分频)

  6. 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)

  7. 可选校验位(无校验/奇校验/偶校验)

  8. USART框图

  9. USART基本结构

    在这里插入图片描述

    时钟信号PCLK进入波特率发生器产生波特率信号。

    当接收信号,GPIO_RX将数据存入接收移位寄存器,当寄存器位满,转入接收寄存器RDR,此时会产生接收完成标志位,该标志位可用于中断操作。

    接发送信号,发送数据寄存器中的数据将会逐步进入发送移位寄存器,当移位寄存器满,会产生发送完成标志位,可用于产生中断。

    9-3 USART串口数据包

    将期望发送的数据打包发送,而不是一个字节一个字节地发送。

    1. HEX数据包

      在这里插入图片描述

      当数据包与包头包尾冲突,采用固定包长格式;当数据包与包头包尾不冲,采用可变包长格式

    2. 文本数据包

      在这里插入图片描述

      文本数据包地数据也是从HEX数据转码得到的

    3. USART串口数据的接收与发送需要注意建立标志位,确保当一次接收/发送完成后,再进行下一轮的接收与发送,避免接收/发送的数据被覆盖

      //发送数据的标志位确立
      USART_GetFlagStatus(USART1,USART_FLAG_TXE) = SET,发送数据寄存器非空
      //接收数据的标志位确立:
      USART_GetFlagStatus(USART1,USART_FLAG_RXE) = SET,接收数据寄存器非空
      //当利用接收数据时产生中断,首先要使能USART的中断,然后当USART_FLAG_RXE置位后,接收中断标志位USART_IT_RXNE自动触发,同时拉起中断程序执行。
      

10-1 I2C通信

  1. I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线

  2. 特点

    1. 包含两根通信线:SCL(Serial Clock)、SDA(Serial Data)
    2. 同步,半双工
    3. 带数据应答
    4. 支持总线挂载多设备(一主多从、多主多从)
  3. 硬件电路

    在这里插入图片描述

    1. SDA和SCL使用原则:SCL在任意时候都是由主机完全掌控;SDA大部分情况下受到主机的控制,只有在从机发送数据和从机应答的时候接收从机的控制。

    2. SDA线中,主机和从机输入输出模式的设置:主机和从机都存在输入输出的情况,如下图所示,若主机输出高电平,从机输出低电平,那么此时电源短路;为避免这个问题,I2C禁止所有设备输出强上拉的高电平,因此输出模式不设置为推挽输出,而是开漏输出

    3. 上拉电阻的作用:由于设置了开漏输出,无法设置高电平,因此在芯片外部接了一个上拉电阻;当被控IC的晶体管关闭,呈现高阻态,此时外部信号通过上拉电阻影响变成高电平。

    4. 被控IC的输出原理:当设备需要发送数据时,它会通过 SDA 线输出数据。对于开漏输出,设备通过将 SDA 线拉低来表示低电平,通过释放 SDA 线(使其处于高阻态)来表示高电平。

    5. 被控IC的输入原理:当设备需要接收数据时,它会将 SDA 线设置为输入模式。此时,设备通过检测 SDA 线上的电平变化来读取数据。从设备还需要检测 SCL 线上的时钟信号,以便在正确的时间点读取或发送数据。

    6. SDA线中的线与现象:

      1. 当所有连接到SDA线上的设备都没有将SDA线拉低时,SDA线通过上拉电阻被拉到高电平(VCC)。此时,SDA线的电平状态为高电平(逻辑1)。
      2. 当任何一个设备将SDA线拉低时,SDA线的电平状态变为低电平(逻辑0)。即使其他设备没有将SDA线拉低,只要有一个设备将SDA线拉低,整个SDA线的电平状态就会被拉低。
  4. I2C的时序单元

    1. 起始和终止单元

      在这里插入图片描述

      当主机发起数据的收发请求开始,主机要在SCL高电平期间,SDA从高电平切换到低电平。

      当主机结束数据收发的请求,主机会在SCL高电平器件从低电平切换到高电平。

    2. 发送一个字节
      在这里插入图片描述

      在I2C中,数据的收发基于SCL时钟线进行,当SCL高电平时,从机才会获取SDA的高低电平状态。因此当主机发送一个字节,需要将SDA上的数据位先置位,然后释放SCL(置高电平),在SCL高电平期间,SDA不允许有电平的变化。

    3. 接收一个字节

在这里插入图片描述

  主机接收数据,此时操控SDA的是从机,从机依次将数据位放到SDA线上。主机在接收之前,需要释放SDA的控制权给从机。
  1. 应答机制

    在这里插入图片描述

    (这里的发送和接收主语是主机)

    **接收应答:**当主机释放SDA控制权,如果从机无人应答,那么SDA被上拉电阻置高电平(数据1),如果从机有人应答(开漏输出置低电平,数据0)

  2. I2C的时序

    1. 指定地址写

      在这里插入图片描述

    I2C统数据的收发单位时字节,所以除了发送和接收应答位,其他部分要发送一个字节的数据。

    起始条件(SCL高电平时,SDA置低电平)——【指定设备(设备地址)——读写位(0表示主机进行写操作,1表示主机进行读操作)】——接收应答(主机释放SDA控制权,等待从机应答)——指定地址(从机的寄存器地址/指令控制字)——接收应答(主机释放SDA控制权,等待从机应答)——主机想要写入的数据——接收应答(主机释放SDA控制权,等待从机应答)——终止条件

    1. 当前地址读

      在这里插入图片描述

      注意:当前地址指针是指,从机内存在一个单独的指针变量指示着寄存器的地址(初始为0,指示第一个寄存器),当寄存器结束了读写操作后,指针会自增一次,指向下一个寄存器。(寄存器地址不确定,少用)

      起始条件(SCL高电平时,SDA置低电平)——【指定设备(设备地址)——读写位(0表示主机进行写操作,1表示主机进行读操作)】——接收应答(主机释放SDA控制权,等待从机应答)——(主机交出SDA控制权,给从机输出主机想要的数据)输出数据——接收应答(从机交还SDA控制权,等待主机应答)——主机想要写入的数据——接收应答(主机释放SDA控制权,等待从机应答)——终止条件

    2. 指定地址读

      在这里插入图片描述

      这是一个复合时序结构,包含了前面两种时序,为什么要做成这样的拼接结构:I²C 协议规定,在一次通信中,主机不能直接从一个寄存器读取数据,而是需要先指定寄存器地址。

      主机先发送写指令的目的是为了指定从设备内部的寄存器地址。再另起一个时序,切换读写方向。

      起始条件(SCL高电平时,SDA置低电平)——【指定设备(设备地址)——写(0表示主机进行写操作,1表示主机进行读操作)】——接收应答(主机释放SDA控制权,等待从机应答)——重新开启时序(因为切换读写操作只能在起始条件后的第一个字节进行切换)——【指定设备(设备地址)——读(0表示主机进行写操作,1表示主机进行读操作)】——接收应答(主机释放SDA控制权,等待从机应答)——(主机交出SDA控制权,给从机输出主机想要的数据)输出数据——接收应答(从机交还SDA控制权,等待主机应答)——终止条件

    3. 总结

      前面三种时序演示了单个字节的时序,如果要读/写多个字节,就在数据后面增加相同格式的字节就可以(读:因为从机内的寄存器指针再执行完一次读写操作后会自增,因此读/写多个字节的操作是针对连续寄存器下的数据的操作),但是在最后一个字节中,主机的接收/发送应答要给非应答,说明主机不想继续进行读/写操作了。

10-3 软件写入I2C

10-4 硬件写入I2C

  1. 软件写入I2C就是在程序层面手动设置SCL与SDA的位操作,实现I2C时序;在硬件层面,就是使用stm32的I2C外设,用硬件生成I2C时序信号。

  2. I2C外设

    在这里插入图片描述

  3. I2C外设电路图

    在这里插入图片描述

    与串口通信(全双工)不同,I2C通信(半双工)只有一个移位寄存器进行数据的收发

    在这里插入图片描述

    与软件写入I2C一样,GPIO口要设置为开漏输出模式,此时GPIO口仍能进行输入,观察电路图如下

    红色线是输入,蓝色线是输出,因此开漏输出模式不影响GPIO口的输入。

11-1 SPI通信

  1. I2C的优势:在消耗最低硬件资源的情况下实现最多的功能。在硬件上,无论挂载多少个设备,都只需要两根数据线,在软件上,数据双向通信,应答位都可以实现 。

  2. I2C的缺点 :I2C从机接口是开漏输出的模式+外上拉电阻的模式,使得数据线上拉高电平的能力较弱(从低电平到达高电平的时间长),这样就会限制I2C的通信速度。

  3. SPI的优势:传输速度更快。

    1. SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
    2. 四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)
    3. 同步,全双工
    4. 支持总线挂载多设备(一主多从
  4. SPI的硬件电路

    在这里插入图片描述

    1. 所有SPI设备的SCK、MOSI、MISO分别连在一起
    2. 主机另外引出多条SS控制线,分别接到各从机的SS引脚
    3. 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
  5. SPI的数据移位示意图(SPI的数据交换原理

    在这里插入图片描述

    经过8次相同的转换流程就能将主机和从机的数据交换(因为SPI采用全双工模式,因此输入输出可以同步进行)

  6. SPI时序基本单元

    1. 起始和终止条件

      在这里插入图片描述

    2. 交换字节时序

      在这里插入图片描述

      在这里插入图片描述

      在这里插入图片描述

      在这里插入图片描述

      CPOL和CPHA(时钟相位)是SPI某个寄存器的设置位。

      模式0(下降沿切换电平,上升沿采集电平)使用的频率最高,能够在SCK的第一个边沿之前就将数据移出,能够提高运行效率。

  7. SPI的时序

    在这里插入图片描述

    基于模式0的交换字节模式,与I2C不同,SPI无需进行应答位传递。

11-2 SPI通信外设

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

  2. 时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)(SPI的时钟也是由外设时钟分频得到

  3. SPI框图

    在这里插入图片描述

    可以看到SPI中存在移位寄存器,发送缓冲取(TDR寄存器),接收缓冲区(RDR寄存器);TDR和RDR是同一个寄存器,

    当进行SPI通信时,流程是这样的:待发送数据(一个字节)写入发送缓冲区(TDR),当移位寄存器空,则写入移位寄存器,此时置状态寄存器TXE=1,当时钟边沿变化,逐位移出,同时SPI接收的数据逐位写入移位寄存器,当移位寄存器写满(一个字节的输入数据),则传入接收缓冲区(RDR),同时在发送缓冲区新的数据又传入了移位寄存器中,此时RXNE=1,此时可以用DMA进行数据转运。

  4. SPI基本结构

    在这里插入图片描述

  5. 连续传输模式与非连续传输模式

    在这里插入图片描述

在这里插入图片描述

​ 区别在于:连续模式中,当发送数据寄存器的值传入移位寄存器后,新的数据马上传递到移位寄存器中;非连续模式中:当发送数据寄存器的值传入移位寄存器后,需等待接受的字节数据接收完成,并且RXNE=1后,新的数据才会传到移位寄存器,这也是SPI进行一个字节数据收发的过程

  1. 软件SPI和硬件SPI的波形区别

    硬件SPI的波形变化紧贴时钟边沿变化,而软件的紧贴现象不明显

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐