Stm32学习笔记,3万字超详细

04-27 1755阅读 0评论

Stm32学习笔记

文章目录

  • Stm32学习笔记
    • 前言的前言
    • 前言
    • 笔记
      • Stm32 三种开发方式的区别
      • 为什么Stm32初始化外设都需要先打开时钟
      • GPIO八种模式
      • Stm32寄存器映射
      • Stm32中的位段映射
      • Stm32中的时钟系统
      • Stm32外设
      • Stm32的端口复用与重映射
      • Stm32中断
      • Stm32的USART使用
      • Stm32的外部中断 (EXTI)
      • Stm32库函数 EXTI_GetFlagStatus 和 EXTI_GetITStatus 区别
      • Stm32的电源控制 (PWR)
      • Stm32中的备份寄存器 (BKP)
      • Stm32中的实时时钟 (RTC)
      • Stm32的低功耗模式
      • Stm32的模数转换(ADC)
      • Stm32中的直接存储器存取 (DMA)
      • Stm32中的集成电路总线 (I2C/IIC)
      • Stm32中的串行外设接口 (SPI)使用
      • Stm32中的控制器局域网 (bxCAN)使用

        前言的前言

        文章的原标题是【Stm32学习笔记】,但是在这个浮躁的时代,不当个标题狗是不会有人点进来的。而既然是发布出来了,那肯定是想要别人点个赞,点个收藏关注一下的,所以在发布的时候还是换了一个浮夸点的标题了。

        前言

        本文章主要记录本人在学习stm32过程中的笔记,也插入了不少的例程代码,方便到时候CV。绝大多数内容为本人手写,小部分来自stm32官方的中文参考手册以及网上其他文章;代码部分大多来自江科大和正点原子的例程,注释是我自己添加;配图来自江科大/正点原子/中文参考手册。

        笔记内容都是平时自己一点点添加,不知不觉都已经这么长了。其实每一个标题其实都可以发一篇,但是这样搞太琐碎了,所以还是就这样吧。

        喜欢的话,就点赞收藏关注一下~

        本人技术有限,如有错误,欢迎在评论区或者私信指点。

        笔记

        本笔记内容以 Stm32F103xx 型号为研究对象。

        Stm32 三种开发方式的区别

        • 寄存器模式:最底层的开发,运行速度最快。实际上也是使用了固件库,但是不是使用固件库的函数,而是使用了固件库的定义,包括宏定义,结构体定义。和51的开发差不多,但因为32的寄存器太多,实际开发手动配置大量寄存器极其耗费时间,同时在没有注释的情况下可读性差,所以较少使用。
        • 标准库模式:基于寄存器进行了函数的封装,而由于函数封装以及内部大量的检查参数有效性的代码,运行速度相对于寄存器模式较慢。封装之后可以根据函数名字就能明白代码作用,容易记忆,使用方便,所以较多人使用。
        • HAL库模式:全称是Hardware Abstraction Layer(抽象印象层),相比于标准库更加深入的封装,有句柄、回调函数等概念(ps:有点类似Windows开发),因此相对于标准库模式有更好的可移植性(可在不同芯片的移植),但代价就是更多的性能损失。

          说明:运行速度,性能损失的问题,都只是相对问题,实际上大多数情况下都可以忽略。

          为什么Stm32初始化外设都需要先打开时钟

          • 每个外设都有独立时钟,如果不打开时钟外设就不能用,原因就是为了低功耗节省用电,不用的外设可以不打开时钟

            开启外设时钟的方法:

            /*
            	AHB外设总线:
            	DMA1,DMA2,SRAM,FLITF,CRC,FSMC,SDIO
            */
            RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC,ENABLE);
            RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC,DISABLE);
            /*
            	APB1外设总线:
            	TIM2,TIM3,TIM4,TIM5,TIM6,TIM7,TIM12,TIM13,TIM14,WWDG
            	SPI2,SPI3,USART2,USART3,UART4,UART5,I2C1,I2C2,USB,CAN1,CAN2,BKP,PWR,DAC,CEC,
            */
            RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2,ENABLE);
            RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2,DISABLE);
            /*
            	APB2外设总线:
                AFIO,GPIOA,GPIOB,GPIOC,GPIOD,GPIOE,GPIOF,GPIOG,ADC1,ADC2
                TIM1,SPI1,TIM8,USART1,ADC3,TIM15,TIM16,TIM17,TIM9,TIM10,TIM11
            */
            RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
            RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, DISABLE);
            

            GPIO八种模式

            模式介绍
            浮空输入GPIO_Mode_IN_FLOATING若引脚悬空,则电平不确定
            上拉输入GPIO_Mode_IPU内部连接上拉电阻,悬空时默认高电平
            下拉输入GPIO_Mode_IPD内部连接下拉电阻,悬空时默认低电平
            模拟输入GPIO_Mode_AINGPIO无效,引脚直接接入内部ADC
            开漏输出GPIO_Mode_Out_OD高电平为高阻态,低电平接VSS(负极)
            推挽输出GPIO_Mode_Out_PP高电平接VDD,低电平接VSS
            复用开漏输出GPIO_Mode_AF_OD由片上外设控制,高电平为高阻态,低电平接VSS
            复用推挽输出GPIO_Mode_AF_PP由片上外设控制,高电平接VDD,低电平接VSS

            高阻态是一个数字电路里常见的术语,指的是电路的一种输出状态,既不是高电平也不是低电平,如果高阻态再输入下一级电路的话,对下级电路无任何影响,和没接一样,如果用万用表测的话有可能是高电平也有可能是低电平,随它后面接的东西定的。

            电路分析时高阻态可做开路理解。你可以把它看作输出(输入)电阻非常大。它的极限状态可以认为悬空(开路)。也就是说理论上高阻态不是悬空,它是对地或对电源电阻极大的状态。而实际应用上与引脚的悬空几乎是一样的。

            开漏输出和推挽输出的区别主要是开漏输出只可以输出强低电平,高电平得靠外部电阻拉高。输出端相当于三极管的集电极,适合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内);推挽输出可以输出强高、低电平,连接数字器件。

            建议看:推挽 开漏 高阻 这都是谁想出来的词??

            更加详细请看:GPIO口8种模式详解

            Stm32学习笔记,3万字超详细 第1张

            Stm32寄存器映射

            以最简单的GPIO讲,将 GPIOA 相关的固件库代码拿出来变很容易明白。

            #define PERIPH_BASE           ((uint32_t)0x40000000)		//外设基地址
            #define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)		//APB2总线基地址
            #define GPIOA_BASE            (APB2PERIPH_BASE + 0x0800)	//GPIOA 基地址
            typedef struct
            {
              __IO uint32_t CRL;
              __IO uint32_t CRH;
              __IO uint32_t IDR;
              __IO uint32_t ODR;
              __IO uint32_t BSRR;
              __IO uint32_t BRR;
              __IO uint32_t LCKR;
            } GPIO_TypeDef;
            #define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)	//GPIOA结构
            

            很明显可以看出来,固件库代码的条理非常清晰,而且非常巧妙。除了第一个外设基地址是固定值,其他的基地址都是通过 上一级基地址+偏移 计算出来的,最后GPIOA是一个 指定地址强制转换结构。

            这样我们如果想要操作寄存器,则可以用

            GPIOA->CRL&=0xFF0FFFFF; 	//将寄存器 20~23位 置0
            GPIOA->CRL|=0x00300000; 	//设置寄存器 20~23位,实际作用是设置PA5为推挽输出
            GPIOA->ODR|=1
                //使用之前需要先启用外设 USART1,GPIOA
            	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
            	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
            	
                //初始化TX引脚 PA9 为复用推挽输出 
            	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);
            	
                //初始化RX引脚 PA10 为上拉输入
            	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);
            	
                //初始化 USART1 为波特率9600,无硬流控,需要收发,无校验,1位停止位
            	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;
            	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
            	USART_Init(USART1, &USART_InitStructure);
            	
                //开启RXNE标志位到NVIC的输出
            	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
            	
                //设置优先级分配配置
            	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
            	
                //配置 USART1 的中断
            	NVIC_InitTypeDef NVIC_InitStructure;
            	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
            	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
            	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
            	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
            	NVIC_Init(&NVIC_InitStructure);
            	
                //最后使能 USART1
            	USART_Cmd(USART1, ENABLE);
            }
            void Serial_SendByte(uint8_t Byte)
            {
            	USART_SendData(USART1, Byte);		//填充数据至 USART1的DR寄存器
                
                //USART_FLAG_TXE: 发送寄存器为空标志位。对USART_DR的写操作时,将该位清零。
            	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	//等待发送完成
            }
            //USART1 中断函数
            void USART1_IRQHandler(void)
            {
            	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
            	{
            		uint8_t Serial_RxData = USART_ReceiveData(USART1);		//读取 USART1 收到的字节
            		
                    /*
                        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
                        这里可以省略手动清除标志位,因为对USART_DR的读操作可以将该位清零。
                    */
            	}
            }
            
                //使能GPIOA时钟
            	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
                //因为使用到了AFIO的中断引脚选择功能,所以要使能AFIO的时钟
            	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
            	
            	GPIO_InitTypeDef GPIO_InitStructure;
            	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
            	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
            	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
            	GPIO_Init(GPIOA, &GPIO_InitStructure);
            	
                //实际上是对AFIO进行操作:将PA14信号输出至EXTI的14号线
            	GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource14);
            	
                //初始化EXTI
            	EXTI_InitTypeDef EXTI_InitStructure;
            	EXTI_InitStructure.EXTI_Line = EXTI_Line14;
            	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
            	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;			//使用中断
            	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;		//下降沿触发
            	EXTI_Init(&EXTI_InitStructure);
                
            	//设置优先级分配配置
            	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
            	
                //配置外部中断
            	NVIC_InitTypeDef NVIC_InitStructure;
            	NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
            	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
            	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
            	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
            	NVIC_Init(&NVIC_InitStructure);
            }
            //中断函数
            void EXTI15_10_IRQHandler(void)
            {
            	if (EXTI_GetITStatus(EXTI_Line14) == SET)
            	{
                    
                     //清除中断标志位
            		EXTI_ClearITPendingBit(EXTI_Line14);
            	}
            }
            
              FlagStatus bitstatus = RESET;
              /* Check the parameters */
              assert_param(IS_GET_EXTI_LINE(EXTI_Line));
              
              if ((EXTI-PR & EXTI_Line) != (uint32_t)RESET)
              {
                bitstatus = SET;
              }
              else
              {
                bitstatus = RESET;
              }
              return bitstatus;
            }
            ITStatus EXTI_GetITStatus(uint32_t EXTI_Line)
            {
              ITStatus bitstatus = RESET;
              uint32_t enablestatus = 0;
              /* Check the parameters */
              assert_param(IS_GET_EXTI_LINE(EXTI_Line));
              
              enablestatus =  EXTI->IMR & EXTI_Line;
              if (((EXTI->PR & EXTI_Line) != (uint32_t)RESET) && (enablestatus != (uint32_t)RESET))
              {
                bitstatus = SET;
              }
              else
              {
                bitstatus = RESET;
              }
              return bitstatus;
            }
            

            可以很容易看出来,代码上的区别在:

            EXTI_GetFlagStatus 部分:
            if ((EXTI->PR & EXTI_Line) != (uint32_t)RESET)
            EXTI_GetITStatus 部分:
            enablestatus =  EXTI->IMR & EXTI_Line;
            if (((EXTI->PR & EXTI_Line) != (uint32_t)RESET) && (enablestatus != (uint32_t)RESET))
            

            即 EXTI_GetITStatus 的判断多了一个条件。

            由手册可以知道:

            EXTI->PR 是 挂起寄存器,0:没有发生触发请求;1:发生了选择的触发请求

            EXTI->IMR 是 中断屏蔽寄存器,0:屏蔽来自线x上的中断请求; 1:开放来自线x上的中断请求。

            Stm32学习笔记,3万字超详细 第2张

            因此,EXTI_GetFlagStatus 只是纯粹读取中断标志位的状态,但是实际上这并不准确,因为设置 EXTI_IMR 寄存器可以对该中断进行屏蔽;而 EXTI_GetITStatus 除了读取中断标志位,还查看 EXTI_IMR 寄存器是否对该中断进行屏蔽。

            另外,EXTI_ClearFlag 和 EXTI_ClearITPendingBit 则是什么区别都没有,内部代码完全一样。

            Stm32的电源控制 (PWR)

            Stm32的工作电压(VDD)为2.0~3.6V。通过内置的电压调节器提供所需的1.8V电源。 当主电源VDD掉电后,通过VBAT脚为实时时钟(RTC)和备份寄存器提供电源。实际上,VBAT脚还可以为 LSE振荡器 和 PC13~PC15 端口供电,可以保证当主电源被切断时RTC能继续工作。但当使用VBAT供电时,PC13~PC15无法用作GPIO。

            管脚名称主功能 (复位后默认)复用功能功能
            PC13PC13TAMPER / RTC用于侵入检测,RTC校准时钟、RTC闹钟或秒输出
            PC14PC14OSC32_INLSE引脚
            PC15PC15OSC32_OUTLSE引脚

            一般来说,VBAT脚接一个纽扣电池供电,如正点原子的开发板。

            Stm32学习笔记,3万字超详细 第3张

            从图中可以看出来,除了上面说到的之外,RCC_BDCR 寄存器也在后备供电区域内。但实际上,RCC_BDCR 寄存器只有 LSEON (外部低速振荡器使能)、LSEBYP (外部低速时钟振荡器旁路)、RTCSEL (RTC时钟源选择) 和 RTCEN (RTC时钟使能)位处于备份域。另外的 LSERDY (外部低速LSE就绪) 与 BDRST (备份域软件复位) 不处于备份域,因为没有必要。

            Stm32中的备份寄存器 (BKP)

            备份寄存器拥有以下特性

            • 当VDD电源被切断,他们仍然由VBAT维持供电。
            • 20字节数据后备寄存器(中容量和小容量产品),或84字节(42*16 Bit)数据后备寄存器(大容量和互联型 产品)
            • 当系统在待机模式下被唤醒,或系统复位或 电源复位时,他们也不会被复位。
            • BKP寄存器是16位的可寻址寄存器,可以用半字(16位)或字(32位)的方式操作这些外设寄存器。

              备份寄存器的复位

              • 软件复位,备份区域复位可由设置备份域控制寄存器 (RCC_BDCR)中的 BDRST位产生
              • 在VDD和VBAT两者都掉电的情况下,VDD或VBAT上电将引发备份区域复位。

                后备区域的保护

                在复位之后,对 后备区域(备份寄存器和RTC) 的访问将被禁止,后备区域被保护以防止可能存在的意外的写操作。

                需要执行以下操作可以使能对后备区域的访问。

                1. 通过设置寄存器 RCC_APB1ENR 的 PWREN 和 BKPEN 位来打开电源和后备接口的时钟
                  • 说人话就是使能 电源控制 (PWR) 与 备份寄存器 (BKP)的时钟
                  • 电源控制寄存器(PWR_CR)的DBP位来使能对后备寄存器和RTC的访问
                /*
                	BKP寄存器基础操作示例
                */
                RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);	//使能PWR和BKP外设时钟
                BKP_ReadBackupRegister(BKP_DR1)				//读取 BKP_DR1 寄存器,启用时钟后就可以读取了
                BKP_DeInit()	//对备份寄存器进行软件复位
                    
                PWR_BackupAccessCmd(ENABLE);	//取消后备区域的写保护,但如果RTC的时钟是HSE/128,无法进行写保护。
                BKP_WriteBackupRegister(BKP_DR1, 0X5050);	 //向 BKP_DR1 寄存器写 0x5050,写之前要取消写保护才可以
                

                Stm32中的实时时钟 (RTC)

                Stm32学习笔记,3万字超详细 第4张

                RTC的本质与定时器类似,就是一个计数器,每秒加一让其可以实现更新时间。

                • RTC的预分配系数最高为2的20次方
                • RTC的计数器是32位的
                • RTC的时钟源可以选择以下三种
                  • RCC_RTCCLKSource_LSE:低速外部时钟
                  • RCC_RTCCLKSource_LSI:低速内部时钟 (通常用这个作为时钟源,32.768 kHz 进行 32768 分配可以得到 1Hz 的时钟信号)
                  • RCC_RTCCLKSource_HSE_Div128:高速外部时钟的128分频
                  • RTC的3个可屏蔽中断
                    • 闹钟中断:用来产生一个软件可编程的闹钟中断
                    • 秒中断:用来产生一个可编程的周期性中断信号(最长可达1秒)
                    • 溢出中断:指示内部可编程计数器溢出并回转为0的状态

                      RTC的时钟源的配置是设置 备份域控制寄存器 (RCC_BDCR) 里的 RTCSEL[1:0] 位。因此,除非备份域复位,不然此选择不能被改变。

                      读RTC寄存器

                      RTC核完全独立于RTC APB1接口。软件通过APB1接口访问RTC的预分频值、计数器值和闹钟值。但是,相关的可读寄存器只在与 RTC APB1时钟进行重新同步的RTC时钟的上升沿被更新。(RTC标志也是如此的)

                      这意味着,如果APB1接口曾经被关闭,而读操作又是在刚刚重新开启APB1之后,则在第一次的内部寄存器更新之前,从APB1上读出的RTC寄存器数值可能被破坏了(通常读到0)。

                      下述几种情况下能够发生这种情形:

                      • 发生系统复位或电源复位
                      • 系统刚从待机模式唤醒
                      • 系统刚从停机模式唤醒

                        所有以上情况中,APB1接口被禁止时(复位、无时钟或断电),RTC核仍保持运行状态。

                        因此,若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待 RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置’1’。

                        写RTC寄存器

                        必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入 RTC_PRL(预分频装载寄存器) 、 RTC_CNT(计数器寄存器) 、 RTC_ALR(闹钟寄存器)。

                        另外,对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询 RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是’1’ 时,才可以写入RTC寄存器。

                        配置过程:

                        1. 查询RTOFF位,直到RTOFF的值变为’1’
                        2. 置CNF值为1,进入配置模式
                        3. 对一个或多个RTC寄存器进行写操作
                        4. 清除CNF标志位,退出配置模式
                        5. 查询RTOFF,直至RTOFF位变为’1’以确认写操作已经完成。

                        仅当CNF标志位被清除时,写操作才能进行,这个过程至少需要3个RTCCLK周期。

                        /*
                        	RTC初始化与中断
                        */
                        u8 RTC_Init(void)
                        {
                        	u8 temp = 0;
                        	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); // 使能PWR和BKP外设时钟
                            PWR_BackupAccessCmd(ENABLE);	// 取消后备区域(RTC和后备寄存器)的写保护
                            
                        	// 判断
                            if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050)
                        	{
                        		BKP_DeInit();					//对备份寄存器进行软件复位
                        		RCC_LSEConfig(RCC_LSE_ON);		 //使能 外设低速晶振
                                
                                //检查指定的RCC标志位设置与否,等待低速晶振就绪
                        		while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET && temp = 250)
                        			return 1;						     //超时说明初始化时钟失败,晶振有问题
                                
                        		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);	    //设置 LSE 作为 RTC时钟源
                        		RCC_RTCCLKCmd(ENABLE);					  //使能RTC时钟,要先设置时钟源
                                
                                 RTC_WaitForSynchro();					   // 等待RTC寄存器同步
                                
                        		RTC_WaitForLastTask();					  // 等待最近一次对RTC寄存器的写操作完成
                        		RTC_ITConfig(RTC_IT_SEC, ENABLE);		   // 使能RTCf的秒中断
                                
                        		RTC_WaitForLastTask();					  // 等待最近一次对RTC寄存器的写操作完成
                        		RTC_SetPrescaler(32767);				  // 设置RTC预分频的值
                                
                        		RTC_WaitForLastTask();					  // 等待最近一次对RTC寄存器的写操作完成
                        		RTC_SetCounter(123456);		   			  // 设置计数值(时间戳)
                                
                            	/*
                                    实际上用不上,因为库函数封装中已经包含,不需要自己手动额外写
                                    RTC_EnterConfigMode();					  // 允许配置
                                    RTC_ExitConfigMode();					  // 退出配置模式
                            	*/
                        		BKP_WriteBackupRegister(BKP_DR1, 0X5050); 	// 向指定的后备寄存器中写入用户程序数据
                        	}
                        	else // 系统继续计时
                        	{
                        		RTC_WaitForSynchro();			  // 等待RTC寄存器同步
                        		RTC_ITConfig(RTC_IT_SEC, ENABLE);  // 使能RTC秒中断
                        		RTC_WaitForLastTask();			  // 等待最近一次对RTC寄存器的写操作完成
                        	}
                            
                            //初始化中断通道
                        	NVIC_InitTypeDef NVIC_InitStructure;
                        	NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn;			  // RTC全局中断
                        	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 先占优先级1位,从优先级3位
                        	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;		  // 先占优先级0位,从优先级4位
                        	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			  // 使能该通道中断
                        	NVIC_Init(&NVIC_InitStructure);	
                            
                        	return 0;
                        }
                        void RTC_IRQHandler(void)
                        {
                        	if (RTC_GetITStatus(RTC_IT_SEC) != RESET) // 秒钟中断
                        	{
                                RTC_WaitForSynchro();		// 等待RTC寄存器同步,读取RTC寄存器前必须做
                        	    RTC_GetCounter();		    // 获取当前计数值(时间戳)
                        	}
                        	if (RTC_GetITStatus(RTC_IT_ALR) != RESET) // 闹钟中断
                        	{
                        		RTC_ClearITPendingBit(RTC_IT_ALR);		// 清闹钟中断
                        		
                        	}
                            
                        	RTC_ClearITPendingBit(RTC_IT_SEC | RTC_IT_OW); // 清秒中断与溢出中断
                        	RTC_WaitForLastTask();
                        }
                        

                        Stm32的低功耗模式

                        Stm32F10xxx有三种低功耗模式:

                        Stm32学习笔记,3万字超详细 第5张

                        WFI:等待中断,如果执行WFI指令进入睡眠模式,任意一个被嵌套向量中断控制器响应的外设中断都能将系统从 睡眠模式唤醒

                        WFE:等待事件,如果执行WFE指令进入睡眠模式,则一旦发生唤醒事件时,微处理器都将从睡眠模式退出

                        除了进行低功耗模式外,还可以在正常运行时使用下面方法降低功耗:

                        • 降低系统时钟
                        • 关闭APB和AHB总线上未被使用的外设时钟

                          睡眠模式:

                          在睡眠模式下,仅停止CPU运作,对于其他外设,将保持原本进入睡眠模式的状态。

                          有两种选项可用于选择睡眠模式进入机制

                          • SLEEP-NOW:如果SLEEPONEXIT位被清除,当WRI或WFE被执行时,微控制器立即进入睡眠模式。
                          • SLEEP-ON-EXIT:如果SLEEPONEXIT位被置位,系统从最低优先级的中断处理程序中退出时,微控制器就立即进入睡眠模式

                            区别就是在于是否处理完当前的中断再进入睡眠,因为一般来说,中断具有很高的实时性,不应该在中断中途进入睡眠。

                            Stm32学习笔记,3万字超详细 第6张

                            停止模式:

                            在停止模式下,除了SRAM(内存)和寄存器内容被保留下来外,其他时钟将会被停止,所有的I/O引脚都保持它们在运行模式时的状态。另外,

                            进入停止模式需要等待闪存编程与APB访问完成,不然会等待完成再进入。

                            当一个中断或唤醒事件导致退出停止模式时,HSI RC振荡器将被选为系统时钟。

                            为了进入停止模式,所有的外部中断的请求位(挂起寄存器(EXTI_PR))和RTC的闹钟标志都必须被清除,否则停止模式的进入流程将会被跳过,程序继续运行。

                            说人话就是要把中断标志清除,不然刚进入停止模式就会被唤醒,相对于没进

                            进入停止模式可以配置以下外设正常运行:

                            1. 独立看门狗(IWDG):可通过写入看门狗的键寄存器或硬件选择来启动IWDG。一旦启动了独立看门狗,除了系统复位,它不能再被停止

                            2. 实时时钟(RTC):通过备份域控制寄存器 (RCC_BDCR)的RTCEN位来设置

                            3. 内部RC振荡器(LSI RC):通过控制/状态寄存器 (RCC_CSR)的LSION位来设置

                            4. 外部32.768kHz振荡器(LSE):通过备份域控制寄存器 (RCC_BDCR)的LSEON位设置

                            5. ADC与DAC:如果在进入该模式前ADC和DAC没有被关闭,那么这些外设仍然消耗电流。通过设置寄存器ADC_CR2 的 ADON 位和寄存器 DAC_CR 的 ENx 位为0可关闭这2个外设

                            6. 电压调节器:可以通过配置电源控制寄存器(PWR_CR)的LPDS位使其运行在正常或低功耗模式。

                              • 若配置电压调节器为低功耗模式,当系统从停止模式退出时,将会有一段额外的启动延时(HSI RC唤醒时间 + 电压调节器从低功耗唤醒的时间)。

                              • 如果在停止模式期间保持内部调节器开启,则退出启动时间会缩短,但相应的功耗会增加。

                            待机模式:

                            待机模式可实现系统的最低功耗,待机模式下只有备份寄存器和待机电路维持供电。从待机唤醒后,差不多和复位一次差不多,除了电源控制/状 态寄存器(PWR_CSR),所有寄存器被复位。SRAM和寄存器内容全部丢失。

                            进入待机模式可以配置正常运行的外设只有停机模式的前四项。

                            在待机模式下,所有的I/O引脚处于高阻态,除了以下的引脚: 复位引脚(始终有效)、当被设置为防侵入或校准输出时的TAMPER引脚、被使能的唤醒引脚

                            简单总结一下:

                            睡眠模式:仅CPU停止运行,GPIO保存进入睡眠之前状态。

                            停止模式:仅保留SRAM(内存)和寄存器的数据,GPIO保存进入睡眠之前状态。

                            待机模式:仅保留备份寄存器,GPIO保持高阻态

                            低功耗模式下的自动唤醒(AWU) :

                            利用RTC可以实现定时唤醒低功耗模式,实际上是使用了RTC的闹钟中断。

                            若要实现低功耗模式下的自动唤醒,RTC的时钟源只能选择:低功耗32.768kHz外部晶振(LSE) 或者 低功耗内部RC振荡器(LSI RC)。

                            为了用RTC闹钟事件将系统从停止模式下唤醒,必须进行如下操作:

                            1. 配置外部中断线17为上升沿触发 (若要从待机模式唤醒则不必配置)
                            2. 配置RTC使其可产生RTC闹钟事件
                            /*
                            	三种模式的进入代码示例
                            */
                            /*进入睡眠模式*/
                            /*
                            	WFI与WFE属于ARM核心指令,库函数中是汇编指令。
                            	SLEEPONEXIT与_SLEEPONEXIT位属于ARM架构的寄存器,在Stm32手册中没有讲到寄存器地址,但是固件库也定义了相关的内容。
                            	进入睡眠模式库函数没有封装,因此只能自己动手丰衣足食。
                            */
                            //理论上SLEEPDEEP位应该是不需要手动清除的,它默认为0,但是为了防止意外情况,就多写一行代码。
                            SCB->SCR &= (uint32_t)~((uint32_t)SCB_SCR_SLEEPDEEP);	//清除深睡眠(SLEEPDEEP)位
                            //根据需要选择是否允许在中断过程中进入睡眠
                            SCB->SCR &= (uint32_t)~((uint32_t)SCB_SCR_SLEEPONEXIT);	//清除SCB_SCR_SLEEPONEXIT位,SLEEP-NOW
                            //SCB->SCR |= SCB_SCR_SLEEPONEXIT;					   //设置SCB_SCR_SLEEPONEXIT位,SLEEP-ON-EXIT
                            __WFI();	//进入等待中断的睡眠。与下面一行二选一即可
                            __WFE();	//进入等待事件的睡眠。
                            /*进入停机模式*/
                            RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);	//使能PWR外设时钟
                            PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI);	//电压调节器开,等待中断模式
                            /*进入待机模式*/
                            RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);	//使能PWR外设时钟
                            PWR_WakeUpPinCmd(ENABLE);  //使能PA0引脚的唤醒管脚功能,如果不需要使用WKUP引脚上升沿唤醒待机可以注释
                            PWR_EnterSTANDBYMode();	   //进入待命(STANDBY)模式
                            

                            Stm32的模数转换(ADC)

                            规则组:用于常规使用

                            注入组:用于突发情况使用ADC功能

                            规则组和注入组的关系有点类似主线程和中断的关系,若触发开始转换注入组可以 对 正在转换的规则组进行插队。

                            输入通道:

                            因为Stm32有双ADC模式(两个ADC配合工作),因此ADC1和ADC2的通道对应的IO基本一样,除了ADC1多出来的温度传感器与内部参考电压通道。

                            通道ADC1ADC2ADC3
                            通道0PA0PA0PA0
                            通道1PA1PA1PA1
                            通道2PA2PA2PA2
                            通道3PA3PA3PA3
                            通道4PA4PA4PF6
                            通道5PA5PA5PF7
                            通道6PA6PA6PF8
                            通道7PA7PA7PF9
                            通道8PB0PB0PF10
                            通道9PB1PB1
                            通道10PC0PC0PC0
                            通道11PC1PC1PC1
                            通道12PC2PC2PC2
                            通道13PC3PC3PC3
                            通道14PC4PC4
                            通道15PC5PC5
                            通道16温度传感器
                            通道17内部参考电压

                            ADC配置:

                            扫描模式:当开始转换后,会根据ADC通道数量(ADC_InitTypeDef.ADC_NbrOfChannel) 按顺序进行N次转换,全部转换完成后设置 EOC(规则组转换结束) 标志位

                            非扫描模式:当开始转换后,仅会对规则组位置一的通道进行1次转换,转换完成设置 EOC 标志位

                            单次转换:在开始转换后,仅仅对规则组整组进行一次转换

                            连续转换:在开始转换后,会循环对规则组整组进行转换

                            间断模式:在开始转换后,进行 N 次转换后停下,并记录当前位置,当下次开始转换时按顺序下去。

                            需要使用 ADC_DiscModeChannelCountConfig 设置 N 的值,并使用 ADC_DiscModeCmd 使能模式。

                            举例: N=3,被转换的通道有 0、1、2、3、6、7、9、10

                            第一次触发:转换的序列为 0、1、2

                            第二次触发:转换的序列为 3、6、7

                            第三次触发:转换的序列为 9、10,并产生EOC事件 (注意这里因为到尾了,所以只转换了两个通道)

                            第四次触发:转换的序列 0、1、2

                            总结一下:

                            如果将ADC转换比喻为使用音乐软件听歌的话

                            ADC_RegularChannelConfig 就是为歌单增加歌曲并设置歌曲的序列

                            ADC_InitTypeDef.ADC_NbrOfChannel 就是歌单中歌曲的数量

                            扫描模式 就是 播放整个歌单的全部歌曲

                            非扫描模式 就是只播放歌单的第一首歌曲

                            单次转换 就是只播放一次 歌单中全部歌曲(扫描模式) / 歌单的第一首歌曲(非扫描模式)

                            连续转换 就是循环播放 歌单中全部歌曲(扫描模式) / 歌单的第一首歌曲(非扫描模式)

                            扫描模式&单次转换 = 歌曲中全部歌曲按顺序全部播放一次

                            非扫描模式&单次转换 = 只播放一次歌单的第一首歌曲

                            扫描模式&连续转换 = 列表循环

                            非扫描模式&连续转换 = 单曲循环

                            间断模式 就是一次听 N 首歌曲,并记下听到第几首了,下次接着听下去,当歌单全部歌曲听完后再回到第一首

                            校准:

                            ADC有一个内置的校准模式,能大幅减少因内部电容器组的变化而造成的准精度误差。因此建议每次上电后都执行一次校准。

                            在 Stm32F10xxx参考手册(2009中文版本) 中ADC章节有这样一句话:

                            启动校准前,ADC必须处于关电状态(ADON=’0’)超过至少两个ADC时钟周期

                            事实上,是ST公司的描写错误,而在官网中找到的 2021 版本中已经被更正为

                            原文:Before starting a calibration, the ADC must have been in power-on state (ADON bit = ‘1’) for at least two ADC clock cycles.

                            翻译:在开始校准之前,ADC必须处于通电状态(ADON位=“1”) 至少两个ADC时钟周期。

                            void AD_Init(void)
                            {
                                //使能时钟
                            	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
                            	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
                            	
                                //配置ADC的时钟周期,RCC_PCLK2_Div6 为高速APB2时钟(PCLK2)的6分频
                            	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
                            	
                                //配置PA0为输入口,模式为模拟输入(GPIO_Mode_AIN),该模式是ADC专用
                            	GPIO_InitTypeDef GPIO_InitStructure;
                            	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
                            	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
                            	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
                            	GPIO_Init(GPIOA, &GPIO_InitStructure);
                            	
                                //配置规则组,将通道0放在第一个位置,采样时间为55.5个周期(ADC_SampleTime_55Cycles5)
                            	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
                            	
                                //初始化ADC1
                            	ADC_InitTypeDef ADC_InitStructure;
                            	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;				    //工作在独立模式
                            	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;				//数据右对齐
                            	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	 //外部触发源选择不使用外部触发
                            	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;					//是否启用连续模式
                            	ADC_InitStructure.ADC_ScanConvMode = DISABLE;					    //是否启用扫描模式
                            	ADC_InitStructure.ADC_NbrOfChannel = 1;							   //进行ADC的通道数量
                            	ADC_Init(ADC1, &ADC_InitStructure);
                            	
                                //使能ADC1
                            	ADC_Cmd(ADC1, ENABLE);
                            	
                                //进行校准
                            	ADC_ResetCalibration(ADC1);								//将校准复位
                            	while (ADC_GetResetCalibrationStatus(ADC1) == SET);		  //等待校准复位完成
                            	ADC_StartCalibration(ADC1);								//开始校准
                            	while (ADC_GetCalibrationStatus(ADC1) == SET);			  //等待校准完成
                            }
                            uint16_t AD_GetValue(void)
                            {
                            	ADC_SoftwareStartConvCmd(ADC1, ENABLE);					//软件触发开始转换
                            	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);   //等待转换完成
                            	return ADC_GetConversionValue(ADC1);					//返回转换得到的数值(0~4095)
                            }
                            

                            Stm32中的直接存储器存取 (DMA)

                            DMA 全程 Direct Memory Access (直接存储器存取),功能就是数据复制,优点就是能代替CPU负责数据复制,让CPU空出来处理其他任务。

                            另外,根据查资料得到,DMA的搬运速度没有CPU搬运的速度快的。详细可以看这里

                            Stm32学习笔记,3万字超详细 第7张

                            数据复制方向支持:存储器到存储器、存储器到外设、外设到存储器。其中因为Flash一般为只读,所以存储器到存储器为 Flash到SRAM 、SRAM到SRAM。

                            数据宽度:

                            支持 字节(Byte,8位)、半字(HalfWord,16位)、字(Word,32位),支持不同宽度的数据复制,复制对齐为低位对齐。例如:半字(0x1122)复制到字节,则会把低八位复制过去,结果为0x22;半字(0x1122)复制字,则会把半字复制到字的低位,结果为0x00001122。

                            地址自增:

                            Stm32学习笔记,3万字超详细 第8张

                            模式:正常模式(复制完就停下)、循环模式(复制完重新开始,循环模式不可用于存储器到存储器)

                            DMA1的请求对应通道:

                            外设通道1通道2通道3通道4通道5通道6通道7
                            ADC1ADC1
                            SPI/I2SSPI1_RXSPI1_TXSPI/I2S2_RXSPI/I2S2_TX
                            USARTUSART3_TXUSART3_RXUSART1_TXUSART1_RXUSART2_RXUSART2_TX
                            I2CI2C2_TXI2C2_RXI2C1_TXI2C1_RX
                            TIM1TIM1_CH1TIM1_CH2TIM1_TX4
                            TIM1_TRIG
                            TIM1_COM
                            TIM1_UPTIM1_CH3
                            TIM2TIM2_CH3TIM2_UPTIM2_CH1TIM2_CH2
                            TIM2_CH4
                            TIM3TIM3_CH3TIM3_CH4
                            TIM3_UP
                            TIM3_CH1
                            TIM3_TRIG
                            TIM4TIM4_CH1TIM4_CH2TIM4_CH3TIM4_UP

                            DMA2的请求对应通道:

                            外设通道1通道2通道3通道4通道5
                            ADC3ADC3
                            SPI / I2S3SPI
                            I2S3_RX
                            SPI
                            I2S3_TX
                            UART4UART4_RXUART4_TX
                            SDIOSDIO
                            TIM5TIM5_CH4TIM5_CH3
                            TIM5_UP
                            TIM5_CH2TIM5_CH1
                            TIM6 / DAC通道1TIM6_UP
                            DAC通道1
                            TIM7 / DAC通道2TIM7_UP
                            DAC通道2
                            TIM8TIM8_CH3
                            TIM8_UP
                            TIM8_CH4
                            TIM8_TRIG
                            TIM8_COM
                            TIM8_CH1TIM8_CH2

                            中断与标志位:

                            中断事件事件标志位使能控制位y=DMA,x=通道
                            传输过半HTIFHTIEDMAy_FLAG_HTx
                            传输完成TCIFTCIEDMAy_FLAG_TCx
                            传输错误TEIFTEIEDMAy_FLAG_TEx

                            DMAy_FLAG_GLx:全局标志,一次性控制三个标志位。

                            /*
                            	DMA 内存到内存 例子
                            */
                            uint16_t MyDMA_Size;	//用于二次开始的时候重置复制次数
                            void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
                            {
                                RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);	//使能DMA1的时钟
                                
                            	MyDMA_Size = Size;	//记录一下,开始复制的时候要设置
                            	
                            	DMA_InitTypeDef DMA_InitStructure;
                            	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;	//外设基地址,当用存储器到存储器时,可写存储器地址
                            	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;	//外设数据宽度
                            	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;		    //外设地址自增
                            	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;						  //存储器基地址
                            	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;		    //存储器数据宽度
                            	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;				   //存储器地址自增
                            	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;	//数据传输方向:SRC外设为源地址,DST外设为目标地址
                            	DMA_InitStructure.DMA_BufferSize = Size;	//需要复制次数,总复制长度=数据宽度*复制次数
                            	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;	//模式:Normal正常模式,Circular循环模式
                            	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;		//是否为存储器到存储器(如果是则只能软件触发开始)
                            	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;	//优先级:z'ji
                            	DMA_Init(DMA1_Channel1, &DMA_InitStructure);		//配置DMA1的通道1,这里因为是存储器到存储器,所以通道可以随便选
                            	
                                //因为还没有给DMA使能,因此没有开始转换
                            }
                            void MyDMA_Transfer(void)
                            {
                            	DMA_Cmd(DMA1_Channel1, DISABLE);	//赋值复制次数之前要失能DMA
                            	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);	//赋值复制次数
                            	DMA_Cmd(DMA1_Channel1, ENABLE);		//使能DMA,开始转换
                            	
                            	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);	//等待复制完成
                            	DMA_ClearFlag(DMA1_FLAG_TC1);	//清除标志位
                            }
                            
                            /*
                            	DMA 外设到存储器 例子
                            	ADC多通道
                            */
                            uint16_t AD_Value[4];		//用于保存ADC转换完成的结果
                            void AD_Init(void)
                            {
                                //使能时钟
                            	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
                            	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
                            	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
                            	
                                //配置ADC时钟频率为APB2时钟的6分频
                            	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
                            	
                                //配置4个IO口
                            	GPIO_InitTypeDef GPIO_InitStructure;
                            	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
                            	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
                            	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
                            	GPIO_Init(GPIOA, &GPIO_InitStructure);
                            	
                                //配置规则组
                            	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为连续扫描模式
                            	ADC_InitTypeDef ADC_InitStructure;
                            	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
                            	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
                            	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
                            	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
                            	ADC_InitStructure.ADC_ScanConvMode = ENABLE;
                            	ADC_InitStructure.ADC_NbrOfChannel = 4;
                            	ADC_Init(ADC1, &ADC_InitStructure);
                            	
                                //具体看上面存储器到存储器例子
                            	DMA_InitTypeDef DMA_InitStructure;
                            	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;	//外设基地址为ADC1的DR寄存器
                            	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
                            	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
                            	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;
                            	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
                            	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
                            	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
                            	DMA_InitStructure.DMA_BufferSize = 4;
                            	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;		//循环模式
                            	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
                            	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
                            	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
                            	
                            	DMA_Cmd(DMA1_Channel1, ENABLE);	//使能时钟,因为非存储器到存储器,所以要硬件请求才能触发开始复制
                            	ADC_DMACmd(ADC1, ENABLE);	    //允许ADC1可以提交请求触发DMA的数据复制
                            	ADC_Cmd(ADC1, ENABLE);		    //使能ADC
                            	
                                //ADC校准
                            	ADC_ResetCalibration(ADC1);
                            	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
                            	ADC_StartCalibration(ADC1);
                            	while (ADC_GetCalibrationStatus(ADC1) == SET);
                            	
                            	ADC_SoftwareStartConvCmd(ADC1, ENABLE);	//软件触发开始转换
                                //因为ADC为连续扫描模式、DMA为循环模式,所以只需要触发开始转换后,硬件就会不断得转换并把数据复制到AD_Value 数组
                            }
                            

                            Stm32中的集成电路总线 (I2C/IIC)

                            在Stm32中使用I2C有两种方案,一是软件模拟I2C,二是硬件I2C。两种方案各有各的优缺点,因此了解清楚才能选择适合的。

                            • 软件模拟I2C
                              • 优点:可以用在任何GPIO口;不会发生卡死(最多出错)
                              • 硬件I2C
                                • 优点:速度比软件模拟快;容易出现卡死的问题

                                  关于硬件I2C卡死问题具体可以看

                                  • 卡死原因分析:浅谈STM32硬件I2C
                                  • 具体测试结论:STM32 硬件I2C 到底是不是个坑?

                                    总结一下Stm32的硬件I2C问题:

                                    1.当时钟频率太高时容易出问题,出问题的概率和时钟频率成正比。

                                    2.当存在中断会打断硬件IIC工作时(中断会导致),容易出现问题。

                                    硬件I2C的发送流程图:

                                    Stm32学习笔记,3万字超详细 第9张

                                    硬件I2C的接收流程图:

                                    Stm32学习笔记,3万字超详细 第10张

                                    /*
                                    	Stm32 使用 硬件I2C 作为主机发送/接收 示例代码
                                    */
                                    #define OLED_ADDRESS	0x78	//定义一个OLED模块的从机地址
                                    void I2C_Config(void)
                                    {
                                    	//使能I2C与GPIO时钟
                                    	RCC_APB1PeriphClockCmd (RCC_APB1ENR_I2C1EN, ENABLE);
                                      	RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOB, ENABLE);
                                    	
                                    	//初始化GPIO,配置PB6与PB7为复用开漏输出
                                    	GPIO_InitTypeDef GPIO_InitStructure;
                                     	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
                                    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
                                    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
                                     	GPIO_Init (GPIOB, &GPIO_InitStructure);
                                    	
                                    	//开始初始化I2C
                                    	I2C_InitTypeDef I2C_InitStructure;
                                    	//使用I2C模式,因为Stm32的I2C硬件外设支持扩展SMBus协议,因此要指定I2C模式
                                    	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
                                    	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;	//七位从机地址
                                    	I2C_InitStructure.I2C_OwnAddress1 = 0x11;		//自己作为从机时的地址
                                    	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;		//默认发送应答
                                    	//配置时钟线(SCL)占空比为低高电平之比为2,仅在I2C的高速模式(100~400 kHz)下有效,标准模式下为1:1
                                    	//原因是SCL低电平时需要变化SDA电平,因此需要更多时间 
                                    	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
                                    	//时钟频率,单位Hz,400000 => 400kHz
                                    	I2C_InitStructure.I2C_ClockSpeed = 400000;
                                    	I2C_Init (I2C1, &I2C_InitStructure);
                                    	I2C_Cmd (I2C1, ENABLE);
                                    }
                                    //封装一个函数用于等待标准事件,包含超时返回,避免卡死
                                    void I2C_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
                                    {
                                    	uint16_t t = 10000;
                                    	while(!I2C_CheckEvent(I2Cx, I2C_EVENT) && t-->0);
                                    }
                                    //指定地址写
                                    void I2C_WriteReg(uint8_t RegAddr, uint8_t Data)
                                    {
                                    	//等待总线不繁忙
                                    	while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
                                    	
                                    	//生成一个起始信号
                                    	I2C_GenerateSTART (I2C1,ENABLE);
                                    	I2C_WaitEvent (I2C1, I2C_EVENT_MASTER_MODE_SELECT);	//等待EV5
                                    	
                                    	//发送七位从机地址(OLED_ADDRESS)进行寻找从机。I2C_Direction_Transmitter表示写,会自动设置最低位为1
                                    	I2C_Send7bitAddress (I2C1, OLED_ADDRESS, I2C_Direction_Transmitter);
                                    	I2C_WaitEvent (I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);		//等待EV6
                                    	//发送一个字节(寄存器地址)
                                    	I2C_SendData (I2C1, RegAddr);
                                    	I2C_WaitEvent (I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING);	//等待EV8
                                    	
                                    	//发送一个字节(数据)
                                    	I2C_SendData(I2C1, Data);
                                    	I2C_WaitEvent (I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED);	//等待EV8_2
                                    	
                                    	//生成停止信号
                                    	I2C_GenerateSTOP(I2C1, ENABLE);
                                    }
                                    //指定地址读
                                    uint8_t I2C_ReadReg(uint8_t RegAddress)
                                    {
                                    	uint8_t Data;
                                        //等待总线不繁忙
                                    	while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
                                        
                                        //生成一个起始信号
                                    	I2C_GenerateSTART(I2C2, ENABLE);
                                    	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);		//等待EV5
                                    	
                                        //发送七位从机地址(OLED_ADDRESS)进行寻找从机。I2C_Direction_Transmitter表示写,会自动设置最低位为1
                                    	I2C_Send7bitAddress(I2C2, OLED_ADDRESS, I2C_Direction_Transmitter);
                                    	I2C_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
                                        
                                    	//发送一个字节(寄存器地址)
                                    	I2C_SendData(I2C2, RegAddress);
                                    	I2C_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);		//等待EV8_2
                                    	
                                        //再次生成起始信号
                                    	I2C_GenerateSTART(I2C2, ENABLE);
                                    	I2C_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);		//等待EV5
                                    	
                                        //发送七位从机地址(OLED_ADDRESS)进行寻找从机。I2C_Direction_Receiver表示读,会自动设置最低位为0
                                    	I2C_Send7bitAddress(I2C2, OLED_ADDRESS, I2C_Direction_Receiver);
                                    	I2C_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);	//等待EV6
                                    	
                                        //需要在接收之前设置为非应答,因为硬件会在接收完后直接发送 应答/非应答,没有等待时间。
                                    	I2C_AcknowledgeConfig(I2C2, DISABLE);
                                        //生成停止信号(但是会在当前字节传输或在当前起始条件发出后产生停止条件,因此可以提前给)
                                    	I2C_GenerateSTOP(I2C2, ENABLE);
                                    	
                                    	I2C_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);	//等待EV7
                                    	Data = I2C_ReceiveData(I2C2);		//读取接收到的数据
                                    	
                                    	I2C_AcknowledgeConfig(I2C2, ENABLE);	//恢复为默认发送应答
                                    	
                                    	return Data;
                                    }
                                    

                                    Stm32中的串行外设接口 (SPI)使用

                                    /*
                                    	SPI使用的示例例子
                                    */
                                    void SPI2_Init(void)
                                    {
                                    	//使能时钟
                                    	RCC_APB2PeriphClockCmd(	RCC_APB2Periph_GPIOB, ENABLE );
                                    	RCC_APB1PeriphClockCmd(	RCC_APB1Periph_SPI2,  ENABLE );
                                     
                                    	//初始化GPIO,配置PB13、PB14、PB15为复用推挽输出
                                     	GPIO_InitTypeDef GPIO_InitStructure;
                                    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
                                    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
                                    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
                                    	GPIO_Init(GPIOB, &GPIO_InitStructure);
                                     	GPIO_SetBits(GPIOB,GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15);  //配置PB13、PB14、PB15为上拉
                                    	//开始 初始化SPI
                                    	SPI_InitTypeDef SPI_InitStructure;
                                    	//设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
                                    	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
                                    	//设置SPI工作模式:设置为主SPI
                                    	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
                                    	//设置SPI的数据大小:SPI发送接收8位帧结构
                                    	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
                                    	//串行同步时钟的空闲状态为高电平
                                    	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
                                    	//串行同步时钟的第二个跳变沿数据被采样
                                    	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
                                    	//NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
                                    	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
                                    	//设置波特率预分频的值:波特率预分频值为256
                                    	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
                                    	//指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
                                    	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
                                    	//CRC值计算的多项式
                                    	SPI_InitStructure.SPI_CRCPolynomial = 7;
                                    	SPI_Init(SPI2, &SPI_InitStructure);  
                                    	SPI_Cmd(SPI2, ENABLE);	//使能SPI外设
                                    	SPI2_ReadWriteByte(0xFF);	
                                    }   
                                    //设置 SPI 的波特率预分频值
                                    void SPI2_SetSpeed(u8 BaudRatePrescaler)
                                    {
                                    	assert_param(IS_SPI_BAUDRATE_PRESCALER(BaudRatePrescaler));
                                    	SPI2->CR1 &= 0XFFC7;			//清零位5:3 
                                    	SPI2->CR1 |= BaudRatePrescaler;	//设置SPI2速度 
                                    	SPI_Cmd(SPI2, ENABLE);
                                    } 
                                    //发送一个数据并收回一个数据
                                    u8 SPI2_ReadWriteByte(u8 TxData)
                                    {		
                                    	u8 retry = 0;
                                    	//检查指定的SPI标志位设置与否:发送缓存空标志位
                                    	while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) {
                                    		retry++;
                                    		if(retry>200)return 0;
                                    	}
                                    	SPI_I2S_SendData(SPI2, TxData); //通过外设SPIx发送一个数据
                                    	
                                    	retry = 0;
                                    	//检查指定的SPI标志位设置与否:接受缓存非空标志位
                                    	while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET){
                                    		retry++;
                                    		if(retry>200)return 0;
                                    	}
                                    	return SPI_I2S_ReceiveData(SPI2); //返回通过SPIx最近接收的数据					    
                                    }
                                    

                                    Stm32中的控制器局域网 (bxCAN)使用

                                    Stm32中的CAN架构:

                                    • 设置

                                      • 速率:CAN总线的速率常用的都是125k到500k(一般使用500k),尽管它的最大速率是1Mbps。但明显的是,最大值往往要求环境更加高,导致容易出现问题。
                                      • 工作模式:初始化模式、正常模式、睡眠模式
                                      • 测试模式:静默模式、回环模式、回环静默模式
                                      • 调试模式:当MCU处于调试模式时,Cortex-M3核心处于暂停状态,提供配置,可以使bxCAN继续正常工作或停止工作(CAN是异步通讯,因此需要这个)
                                      • 发送:

                                        • 3个发送邮箱:可以配置发送优先级(按写入先后 / 按标识符数值)
                                        • 自动重传:发送失败则自动重新发送,直至成功
                                        • 接收:

                                          • 2个三级深度接收邮箱(FIFO):共可以接收6个报文

                                          • 注:FIFO是英文First In First Out 的缩写,是一种先进先出的数据缓存器

                                          • 锁定模式:锁定状态下,接收溢出则丢弃;非锁定状态下,接收溢出则覆盖

                                          • 过滤器:

                                            • 14个位宽可配置的标识符过滤器组
                                              • 一个位宽可配置为1个32位掩码模式/2个32位标识符列表模式/2个16位掩码模式/4个16位标识符列表模式
                                              • 过滤模式
                                                • 标识符列表模式:丢弃掉非指定标识符的报文
                                                • 掩码模式:可以指定标识符某些位是非必要的后进行比对

                                                  测试模式图解:

                                                  Stm32学习笔记,3万字超详细 第11张

                                                  过滤器:

                                                  Stm32学习笔记,3万字超详细 第12张

                                                  CAN_Mode_Init()
                                                  {
                                                  	//使能时钟
                                                  	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
                                                  	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
                                                  	//初始化CAN_RX为上拉输入
                                                  	GPIO_InitTypeDef GPIO_InitStructure;
                                                  	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
                                                  	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
                                                  	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
                                                  	GPIO_Init(GPIOA, &GPIO_InitStructure);
                                                  	//初始化CAN_TX为复用推挽输出
                                                  	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
                                                  	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; 
                                                  	GPIO_Init(GPIOA, &GPIO_InitStructure);
                                                  	// CAN单元设置
                                                  	CAN_InitTypeDef CAN_InitStructure;
                                                  	CAN_InitStructure.CAN_TTCM = DISABLE;	//非时间触发通信模式
                                                  	CAN_InitStructure.CAN_ABOM = DISABLE;	//软件自动离线管理
                                                  	CAN_InitStructure.CAN_AWUM = DISABLE;	//睡眠模式通过软件唤醒(清除CAN->MCR的SLEEP位)
                                                  	CAN_InitStructure.CAN_NART = ENABLE; 	//禁止报文自动传送
                                                  	CAN_InitStructure.CAN_RFLM = DISABLE;	//报文不锁定,新的覆盖旧的
                                                  	CAN_InitStructure.CAN_TXFP = DISABLE;	//优先级由报文标识符决定
                                                  	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;	//模式设置: mode:0,普通模式;1,回环模式;
                                                  	// 设置波特率 500kMps
                                                  	CAN_InitStructure.CAN_Prescaler = 4;		//预分频系数
                                                  	CAN_InitStructure.CAN_SJW = CAN_SJW_1tq;	//重新同步跳跃宽度 CAN_SJW_1tq ~ CAN_SJW_4tq
                                                  	CAN_InitStructure.CAN_BS1 = CAN_BS1_9tq;	//CAN_BS1_1tq ~CAN_BS1_16tq
                                                  	CAN_InitStructure.CAN_BS2 = CAN_BS2_8tq;	//CAN_BS2_1tq ~	CAN_BS2_8tq
                                                  	CAN_Init(CAN1, &CAN_InitStructure);
                                                  	CAN_FilterInitTypeDef CAN_FilterInitStructure;
                                                  	CAN_FilterInitStructure.CAN_FilterNumber = 0; 	//过滤器0,可以为0~13
                                                  	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;		//掩码模式
                                                  	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;	//32位
                                                  	CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000;	//32位标识符
                                                  	CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;	
                                                  	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000; //32位掩码,1:要求一致,0:不限制
                                                  	CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;
                                                  	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; // 关联到FIFO0
                                                  	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;	// 使能过滤器0
                                                  	CAN_FilterInit(&CAN_FilterInitStructure); // 滤波器初始化
                                                  /* 	
                                                  	用于开启中断
                                                  	CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); // FIFO0消息挂号中断允许
                                                  	NVIC_InitTypeDef NVIC_InitStructure;
                                                  	NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;
                                                  	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 主优先级为1
                                                  	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;		  // 次优先级为0
                                                  	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
                                                  	NVIC_Init(&NVIC_InitStructure);
                                                  */
                                                  }
                                                  //中断函数模板
                                                  void USB_LP_CAN1_RX0_IRQHandler(void)
                                                  {
                                                  	CanRxMsg RxMessage;
                                                  	int i = 0;
                                                  	CAN_Receive(CAN1, 0, &RxMessage);
                                                  	for (i = 0; i 
                                                                  
                                                                  
                                                                  

免责声明
1、本网站属于个人的非赢利性网站,转载的文章遵循原作者的版权声明。
2、本网站转载文章仅为传播更多信息之目的,凡在本网站出现的信息,均仅供参考。本网站将尽力确保所
提供信息的准确性及可靠性,但不保证信息的正确性和完整性,且不对因信息的不正确或遗漏导致的任何
损失或损害承担责任。
3、任何透过本网站网页而链接及得到的资讯、产品及服务,本网站概不负责,亦不负任何法律责任。
4、本网站所刊发、转载的文章,其版权均归原作者所有,如其他媒体、网站或个人从本网下载使用,请在
转载有关文章时务必尊重该文章的著作权,保留本网注明的“稿件来源”,并白负版权等法律责任。

手机扫描二维码访问

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
评论列表 (暂无评论,1755人围观)

还没有评论,来说两句吧...

目录[+]