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

2023-10-27

Stm32学习笔记

前言的前言

文章的原标题是【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_AIN GPIO无效,引脚直接接入内部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种模式详解

GPIO端口位的基本结构

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<<5;		    //PA5 输出高电平

另外可以注意到,所有地址都是使用了#define定义常量值,这是因为编译器在进行项目编译的时候,对于常量间的计算,是能直接优化成常量值。如:

GPIOA->CRL&=0xFF0FFFFF;
//进行预编译处理之后为:
((GPIO_TypeDef *) ((((uint32_t)0x40000000) + 0x10000) + 0x0800))&=0xFF0FFFFF;
//然后优化为:
((GPIO_TypeDef *) ((uint32_t)0x40010800) &=0xFF0FFFFF;

Stm32中的位段映射

Cortex?-M3存储器映像包括两个位段 (bit-band) 区。这两个位段区将别名存储器区中的每个字映射到位段存储器区的一个位,在别名存储区写入一个字具有对位段区的目标位执行读-改-写操作的相同效果。
在Stm32F10xxx里,外设寄存器和SRAM都被映射到一个位段区里,这允许执行单一的位段的写和读操作。
下面的映射公式给出了别名区中的每个字是如何对应位带区的相应位的:

bit_word_addr = bit_band_base + (byte_offset×32) + (bit_number×4)

其中:

  • bit_word_addr 是别名存储器区中字的地址,它映射到某个目标位。
  • bit_band_base 是别名区的起始地址。
  • byte_offset 是包含目标位的字节在位段里的序号
  • bit_number 是目标位所在位置(0-31)

例子: 下面的例子说明如何映射别名区中SRAM地址为 0x20000300 的字节中的 位2:

0x22006008 = 0x22000000 + (0x300×32) + (2×4).

0x22006008 地址的写操作与对SRAM中地址 0x20000300 字节的 位2 执行读-改-写操作有着相 同的效果。
0x22006008 地址返回SRAM中地址 0x20000300 字节的 位2 的值(0x01 或 0x00)

  • 本节内容摘抄至 Stm32F10xxx参考手册(中文).pdf

Stm32中的时钟系统

Stm32 有5个时钟源:HSI、 HSE、LSI、LSE、PLL

中文名称 解释
HSI 高速内部时钟 RC振荡器,频率为8MHZ,精度不高。
HSE 高速外部时钟 可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHZ~16MHz。
LSI 低速内部时钟 RC振荡器,频率为40kHz,提供低功耗时钟。一般用于看门狗(WDG)
担当一个低功耗时钟源的角色,它可以在停机和待机模式下保持运行,为独立看门狗和 自动唤醒单元提供时钟。
LSE 低速外部时钟 接频率为32.768kHz的石英晶体。一般用于实时时钟(RTC)
PLL 锁相环倍频输出 本质上与其他四个时钟源不一样,这个时钟源是将 时钟输入源 进行 倍频 再输出
时钟输入源可选择为HSI / 2HSEHSE / 2
倍频可选择为2~16倍,但是其输出频率最大不得超过72MHZ。
倍频器的原理:https://www.bilibili.com/video/BV1Mq4y1G77m

Stm32F10XX时钟系统框图

Stm32时钟树

时钟安全系统(CSS)

Stm32中还有一个时钟安全系统(CSS),在出现意外情况下还挺有用的。不过既然说是意外,就说明出现的概率并不大,因此这个功能没有什么存在感。

时钟安全系统可以通过软件被激活。一旦其被激活,时钟监测器将在HSE振荡器启动延迟后被使能,并在HSE时钟关闭后关闭。

如果HSE时钟发生故障,HSE振荡器将被自动关闭,时钟失效事件将被送到高级定时器(TIM1和 TIM8)的刹车输入端,并产生时钟安全中断CSSI,允许软件完成营救操作。此CSSI中断连接到 Cortex?-M3的NMI中断(不可屏蔽中断)。

如果HSE振荡器被直接或间接地作为系统时钟,(间接的意思是:它被作为PLL输入时钟,并且 PLL时钟被作为系统时钟),时钟故障将导致系统时钟自动切换到HSI振荡器,同时外部HSE振荡 器被关闭。在时钟失效时,如果HSE振荡器时钟(被分频或未被分频)是用作系统时钟的PLL的输 入时钟,PLL也将被关闭。

——Stm32F10xxx参考手册(中文).pdf

Stm32外设

Stm32架构

AHB,是Advanced High performance Bus的缩写,高级高性能总线;
APB,是Advanced Peripheral Bus的缩写,高级外设总线。

从图中就可以看出,APB1、APB2都是AHB系统总线进行桥接出来的。另外APB1最高只有36MHz,APB2最高可以达到72MHz。

Stm32的各种外设

  • IO口 (GPIO)
  • 定时器 (TIM)
  • 数模转换器 (DAC)
  • 模数转换器 (ADC)
  • 串口 (UART)
  • 串行外设接口 (SPI)
  • 集成电路总线 (I2C/IIC)
  • 集成电路内置音频总线 (IIS/I2S)
  • 外部中断/事件控制器 (EXTI)
  • 通用和复用功能IO (AFIO)
  • 独立看门狗 (IWDG)
  • 窗口看门狗 (WWDG)
  • 备份寄存器 (BKP)
  • 实时时钟 (RTC)
  • USB全速设备接口 (USB)
  • 控制器局域网 (bxCAN)

内核外设

  • 嵌套中断向量控制器 (NVIC)

Stm32的端口复用与重映射

Stm32有很多的IO口,同时有很多的外设。这些IO口默认是用来做普通的输出输入引脚,而配置为外设需要用到IO口,就叫IO口的复用。如:

管脚名称 主功能 (复位后) 默认复用功能 重定义功能
PA9 PA9 USART1_TX
PA10 PA10 USART1_RX
/*
	以下代码则是配置PA9、PA10为复用。
	其实PA10作为输入引脚,并不区分复用不复用的,因为输出只能有一个外设控制,但是输入可以多个外设读取,不冲突。
*/

//需要使能GPIO和复用外设的时钟,使用默认复用功能时,AFIO时钟不需要使能
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);

每个内置外设都有若干个输入输出引脚,一般这些引脚的输出端口都是固定不变的。但在实际使用中,为了让设计工程师可以更好地安排引脚的走向和功能,在Stm32中引入了外设引脚重映射的概念。即一个外设的引脚除了具有默认的端口外,还可以通过设置重映射寄存器的方式,把这个外设的引脚映射到其它的端口。

管脚名称 主功能 (复位后) 默认复用功能 重定义功能
PB6 PB6 1I2C1_SCL / TIM4_CH1 USART1_TX
PB7 PB7 I2C1_SDA / FSMC_NADV / TIM4_CH2 USART1_RX

如 外设的 USART1_TX 引脚除了PA9外,还可以使用PB6

//使能重映射之后的GPIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
//使能复用外设的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
//重映射需要使能AFIO时钟,因为下一行代码是配置AFIO_MAPR寄存器
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

//实际上是对AFIO进行操作:重映射引脚
GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE);

//初始化PB6与PB7引脚,略
//...

部分重映射&完全重映射
部分重映射:功能外设的部分引脚重新映射,还有一部分引脚是原来的默认引脚
完全重映射:功能外设的所有引脚都重新映射

何时需要使能AFIO时钟?
根据手册说明:对寄存器AFIO_EVCR(事件控制寄存器)、AFIO_MAPR(复用重映射和调试I/O配置寄存器)和AFIO_EXTICRX(外部中断配置寄存器) 进行读写操作前,应当首先打开AFIO 的时钟。

说人话就是在用到 外部中断端口重映射 的时候要使能AFIO时钟

Stm32中断

Stm32F103xx 中有60个可编程外设中断。配置中断的代码如下:

抢占优先级:优先级高的能打断优先级低
响应优先级:当抢占优先级相同时,响应优先级高的先执行

注意:优先级的值越小,优先级越高(越先执行)

总结:抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队

可能有些朋友没办法理解响应优先级的优先排队的作用,那我再解释一下优先排队的概念:

假设一个[抢占优先级=0]的中断①进行过程中,先触发了[抢占优先级=1,响应优先级=2]的中断②,再触发了[抢占优先级=1,响应优先级=1]的中断③

则中断①结束后,理论上应该按照先来后到先执行中断②,然后再执行中断③的,但实际上因为中断③响应优先级更高,因此中断③拥有优先排队(插队)的权限,因此最终是先执行中断③,再执行中断②

#define NVIC_PriorityGroup_0         ((uint32_t)0x700) 		// 0位抢先优先级、4位响应优先级
#define NVIC_PriorityGroup_1         ((uint32_t)0x600) 		// 1位抢先优先级、3位响应优先级
#define NVIC_PriorityGroup_2         ((uint32_t)0x500) 		// 2位抢先优先级、2位响应优先级
#define NVIC_PriorityGroup_3         ((uint32_t)0x400) 		// 3位抢先优先级、1位响应优先级
#define NVIC_PriorityGroup_4         ((uint32_t)0x300) 		// 4位抢先优先级、0位响应优先级

NVIC_PriorityGroupConfig (NVIC_PriorityGroup_2);		//设置优先级分配配置

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);		//初始化中断通道

Stm32的USART使用

void Serial_Init(void)
{
    //使用之前需要先启用外设 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的读操作可以将该位清零。
        */
	}
}

Stm32的外部中断 (EXTI)

外部中断架构_江科大

/*
	配置外部中断的示例代码
*/
void EXTI(void)
{
    //使能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);
	}
}

Stm32库函数 EXTI_GetFlagStatus 和 EXTI_GetITStatus 区别

FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line)
{
  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上的中断请求。

EXTI中断

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

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

Stm32的电源控制 (PWR)

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

管脚名称 主功能 (复位后默认) 复用功能 功能
PC13 PC13 TAMPER / RTC 用于侵入检测,RTC校准时钟、RTC闹钟或秒输出
PC14 PC14 OSC32_IN LSE引脚
PC15 PC15 OSC32_OUT LSE引脚

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

电源框图

从图中可以看出来,除了上面说到的之外,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)的时钟
  2. 电源控制寄存器(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)

RTC的简化框图

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)
		{
			temp++;
			delay_ms(10);
		}
        
		if (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有三种低功耗模式:

3种低功耗模式

WFI:等待中断,如果执行WFI指令进入睡眠模式,任意一个被嵌套向量中断控制器响应的外设中断都能将系统从 睡眠模式唤醒
WFE:等待事件,如果执行WFE指令进入睡眠模式,则一旦发生唤醒事件时,微处理器都将从睡眠模式退出

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

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

睡眠模式

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

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

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

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

电源框图

停止模式

在停止模式下,除了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多出来的温度传感器与内部参考电压通道。

通道 ADC1 ADC2 ADC3
通道0 PA0 PA0 PA0
通道1 PA1 PA1 PA1
通道2 PA2 PA2 PA2
通道3 PA3 PA3 PA3
通道4 PA4 PA4 PF6
通道5 PA5 PA5 PF7
通道6 PA6 PA6 PF8
通道7 PA7 PA7 PF9
通道8 PB0 PB0 PF10
通道9 PB1 PB1
通道10 PC0 PC0 PC0
通道11 PC1 PC1 PC1
通道12 PC2 PC2 PC2
通道13 PC3 PC3 PC3
通道14 PC4 PC4
通道15 PC5 PC5
通道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搬运的速度快的。详细可以看这里

DMA基础结构_江科大

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

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

地址自增

DMA_地址自增示意图

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

DMA1的请求对应通道

外设 通道1 通道2 通道3 通道4 通道5 通道6 通道7
ADC1 ADC1
SPI/I2S SPI1_RX SPI1_TX SPI/I2S2_RX SPI/I2S2_TX
USART USART3_TX USART3_RX USART1_TX USART1_RX USART2_RX USART2_TX
I2C I2C2_TX I2C2_RX I2C1_TX I2C1_RX
TIM1 TIM1_CH1 TIM1_CH2 TIM1_TX4
TIM1_TRIG
TIM1_COM
TIM1_UP TIM1_CH3
TIM2 TIM2_CH3 TIM2_UP TIM2_CH1 TIM2_CH2
TIM2_CH4
TIM3 TIM3_CH3 TIM3_CH4
TIM3_UP
TIM3_CH1
TIM3_TRIG
TIM4 TIM4_CH1 TIM4_CH2 TIM4_CH3 TIM4_UP

DMA2的请求对应通道

外设 通道1 通道2 通道3 通道4 通道5
ADC3 ADC3
SPI / I2S3 SPI
I2S3_RX
SPI
I2S3_TX
UART4 UART4_RX UART4_TX
SDIO SDIO
TIM5 TIM5_CH4 TIM5_CH3
TIM5_UP
TIM5_CH2 TIM5_CH1
TIM6 / DAC通道1 TIM6_UP
DAC通道1
TIM7 / DAC通道2 TIM7_UP
DAC通道2
TIM8 TIM8_CH3
TIM8_UP
TIM8_CH4
TIM8_TRIG
TIM8_COM
TIM8_CH1 TIM8_CH2

中断与标志位

中断事件 事件标志位 使能控制位 y=DMA,x=通道
传输过半 HTIF HTIE DMAy_FLAG_HTx
传输完成 TCIF TCIE DMAy_FLAG_TCx
传输错误 TEIF TEIE DMAy_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问题:
1.当时钟频率太高时容易出问题,出问题的概率和时钟频率成正比。
2.当存在中断会打断硬件IIC工作时(中断会导致),容易出现问题。

硬件I2C的发送流程图

Stm32_I2C_主机发送

硬件I2C的接收流程图

Stm32_I2C_主机接收

/*
	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位标识符列表模式
    • 过滤模式
      • 标识符列表模式:丢弃掉非指定标识符的报文
      • 掩码模式:可以指定标识符某些位是非必要的后进行比对

测试模式图解

CAN测试模式

过滤器

CAN过滤器

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 < 8; i++)
		printf("rxbuf[%d]:%d\r\n", i, RxMessage.Data[i]);
}

//发送报文,返回0为成功,否则失败
u8 Can_Send_Msg(u8 *msg, u8 len)
{
	u8 mbox;
	u16 i = 0;
	CanTxMsg TxMessage;
	TxMessage.StdId = 0x12;			//标准标识符
	TxMessage.ExtId = 0x12;			//设置扩展标示符
	TxMessage.IDE = CAN_Id_Standard; //表明为标准帧
	TxMessage.RTR = CAN_RTR_Data;	 //表明为数据帧
	TxMessage.DLC = len;			//要发送的数据长度
	for (i = 0; i < len; i++)		 //复制数据到结构体
		TxMessage.Data[i] = msg[i];
	mbox = CAN_Transmit(CAN1, &TxMessage);	//填入发送邮箱,mbox为被填入的邮箱号
	i = 0;
	while ((CAN_TransmitStatus(CAN1, mbox) == CAN_TxStatus_Failed) && (i < 0XFFF))
		i++; //等待发送结束
	if (i == 0XFFF)
		return 1;	//超时
	return 0;
}

//接收数据查询,成功返回数据长度,没有返回0
u8 Can_Receive_Msg(u8 *buf)
{
	u32 i;
	CanRxMsg RxMessage;
	if (CAN_MessagePending(CAN1, CAN_FIFO0) == 0)	//查询邮箱有多少条数据
		return 0;
	CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); //读取数据
	for (i = 0; i < 8; i++)
		buf[i] = RxMessage.Data[i];
	return RxMessage.DLC;
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Stm32学习笔记,3万字超详细 的相关文章

  • 我当年自学黑客(网络安全)的一些心得!(内附学习笔记)

    前 言 写这篇教程的初衷是很多朋友都想了解如何入门 转行网络安全 实现自己的 黑客梦 文章的宗旨是 1 指出一些自学的误区 2 提供客观可行的学习表 3 推荐我认为适合小白学习的资源 大佬绕道哈 文末有福利 一 自学网络安全学习的误区和陷阱
  • The Planets:Venus

    靶场下载 The Planets Venus VulnHub 信息收集 arp scan l Interface eth0 type EN10MB MAC 00 0c 29 43 7c b1 IPv4 192 168 1 60 Starti
  • CMSIS & STM32,如何开始? [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我想在 STM32 上使用 CMSIS 启动项目 网上一搜 没找到具体的教程 有些使用 SPL 开始项
  • Arm:objcopy 如何知道 elf 中的哪些部分要包含在二进制或 ihex 中?

    我正在开发一个项目 其中涉及解析arm elf 文件并从中提取部分 显然 elf 文件中有很多部分没有加载到闪存中 但我想知道 objcopy 到底如何知道要在二进制文件中包含哪些部分以直接闪存到闪存中 以arm elf文件的以下reade
  • 【CTF必看】从零开始的CTF学习路线(超详细),让你从小白进阶成大神!

    最近很多朋友在后台私信我 问应该怎么入门CTF 个人认为入门CTF之前大家应该先了解到底 什么是CTF 而你 学CTF的目的又到底是什么 其次便是最好具备相应的编程能力 若是完全不具备这些能力极有可能直接被劝退 毕竟比赛的时候动不动写个脚本
  • SRC漏洞挖掘经验+技巧篇

    一 漏洞挖掘的前期 信息收集 虽然是前期 但是却是我认为最重要的一部分 很多人挖洞的时候说不知道如何入手 其实挖洞就是信息收集 常规owasp top 10 逻辑漏洞 重要的可能就是思路猥琐一点 这些漏洞的测试方法本身不是特别复杂 一般混迹
  • 用户数据中的幸存者偏差

    幸存者偏差 Survivorship bias 是一种常见的逻辑谬误 意思是没有考虑到筛选的过程 忽略了被筛选掉的关键信息 只看到经过筛选后而产生的结果 先讲个故事 二战时 无奈德国空防强大 盟军战机损毁严重 于是军方便找来科学家统计飞机受
  • 嵌入式开发--STM32G4系列片上FLASH的读写

    这个玩意吧 说起来很简单 就是几行代码的事 但楞是折腾了我大半天时间才搞定 原因后面说 先看代码吧 读操作 读操作很简单 以32位方式读取的时候是这样的 data IO uint32 t 0x0800F000 需要注意的是 当以32位方式读
  • 【VUE毕业设计】基于SSM的在线课堂学习设计与实现(含源码+论文)

    文章目录 1 项目简介 2 实现效果 2 1 界面展示 3 设计方案 3 1 概述 3 2 系统流程 3 3 系统结构设计 4 项目获取
  • 库函数点亮Led

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 文章目录 前言 一 pandas是什么 二 使用步骤 1 引入库 2 读入数据 总结 前言 提示 这里可以添加本文要记录的大概内容 例如 随着人工智能的不断发展 机器学习这门
  • 网络安全(黑客)自学

    1 网络安全是什么 网络安全可以基于攻击和防御视角来分类 我们经常听到的 红队 渗透测试 等就是研究攻击技术 而 蓝队 安全运营 安全运维 则研究防御技术 2 网络安全市场 一 是市场需求量高 二 则是发展相对成熟入门比较容易 3 所需要的
  • STM32H5 Nucleo-144 board开箱

    文章目录 开发板资料下载 目标 点亮LD1 绿 LD2 黄 和LD3 红 三个LED灯 开箱过程 博主使用的是STM32CubeMX配置生成代码 具体操作如下 打开STM32CubeMX File gt New project 选择开发板型
  • 【GRNN-RBFNN-ILC算法】【轨迹跟踪】基于神经网络的迭代学习控制用于未知SISO非线性系统的轨迹跟踪(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 2 1 第1部分 2 2 第2部分
  • 为什么我强烈推荐大学生打CTF!

    前言 写这个文章是因为我很多粉丝都是学生 经常有人问 感觉大一第一个学期忙忙碌碌的过去了 啥都会一点 但是自己很难系统的学习到整个知识体系 很迷茫 想知道要如何高效学习 这篇文章我主要就围绕两点 减少那些罗里吧嗦的废话 直接上干货 CTF如
  • 为什么这么多人自学黑客,但没过多久就放弃了(掌握正确的网络安全学习路线很重要)

    网络安全是一个 不断发展和演变 的领域 以下是一个 网络安全学习路线规划 旨在帮助初学者快速入门和提高自己的技能 基础知识 网络安全的 基础知识 包括 网络结构 操作系统 编程语言 等方面的知识 学习这些基础知识对理解网络安全的原理和技术至
  • 【js学习之路】遍历数组api之 `filter `和 `map`的区别

    一 前言 数组是我们在项目中经常使用的数据类型 今天我们主要简述作用于遍历数组的api filter 和 map 的区别 二 filter和map的共同点 首先 我们主要阐述一下 filter 和 map 的共同点 api的参数都是回调函数
  • STM32 上的 ADC 单次转换

    我正在研究 STM32 F103x 上的 ADC 编程 并从最简单的情况 单次转换开始 测量内部温度传感器 连接到 ADC1 的值 并使用 USART 将其发送到 COM 端口 目标似乎很明确 但是当我尝试将源代码下载到闪存时 它不会向 C
  • STM32F0、ST-link v2、OpenOCD 0.9.0:打开失败

    我在用着发射台 http www ti com ww en launchpad about htmlgcc arm none eabi 4 9 2015q2 为 STM32F0 进行编译 现在我想使用该集合中的 arm none eabi
  • 使用 STM32 USB 设备库将闪存作为大容量存储设备

    我的板上有这个闪存IC 它连接到我的STM32F04 ARM处理器 处理器的USB端口可供用户使用 我希望我的闪存在通过 USB 连接到 PC 时被检测为存储设备 作为第一步 我在程序中将 USB 类定义为 MSC 效果很好 因为当我将主板
  • STM32 传输结束时,循环 DMA 外设到存储器的行为如何?

    我想问一下 在以下情况下 STM32 中的 DMA SPI rx 会如何表现 我有一个指定的 例如 96 字节数组 名为 A 用于存储从 SPI 接收到的数据 我打开循环 SPI DMA 它对每个字节进行操作 配置为 96 字节 是否有可能

随机推荐

  • js引入mathjax时注意事项

    首先 保证网络畅通 必须网络好 不然js响应不回来 需要先在head标签中引入js
  • (阿里云)使用WordPress搭建一个专属自己的博客

    一 创建资源 在页面左侧 单击 云产品资源 下拉列表 查看本次实验所需资源 单击屏幕右侧 创建资源 免费创建当前实验所需云产品资源 说明 资源创建过程需要1 3分钟 完成实验资源的创建后 您可以在 云产品资源 列表查看已创建的资源信息 例如
  • 浅析ARM公司在物联网领域的战略布局

    原文地址 http blog csdn net yefanqiu article details 17006331 随着ARM芯片的出货量越来越多 自信满满的ARM公司统一软硬件平台的战略和雄心壮志越来越凸显 最初ARM公司仅是出售自己的知
  • 解决VMware出现“磁盘实用工具不可用”

    可能有人想用vmware的磁盘映射 但是磁盘映射按钮是灰色的 按钮上面还有一行字 磁盘实用工具不可用 我也遇到过这个问题 上网查了一下 结果网上愣是没有一篇博客说明了这个问题 我想 难道这些人从来没遇到过吗 后来 经过自己的实验 我发现了解
  • 一次serialVersionUID引发的血案

    背景 去掉了两个bean类中重写的equals方法 该equals方法只判断id 相同则true 否则false 看了看没有什么地方用到了这个equals 就直接去掉了 测试环境出现异常 org springframework core c
  • sklearn机器学习——day07

    无监督学习 聚类 分类 聚类算法又叫做 无监督分类 其目的是将数据划分成有意义或有用的组 或簇 sklearn当中的聚类算法 有两种表现形式 类 函数 KMeans是如何工作的 重要参数n clusters n clusters是KMean
  • MySQL高级之SQL优化

    福利 网络安全重磅福利 入门 进阶全套282G学习资源包免费分享 https mp weixin qq com s BWb9OzaB gVGVpkm161PMw 5 SQL优化 5 1 大批量插入数据 环境准备 CREATE TABLE t
  • 【Centos】centos7 NFS共享目录(单机版)

    环境介绍 centos 7 三台机器 服务端 192 168 30 13 提供共享目录 客户端 192 168 30 14 192 168 30 15 挂载使用服务端共享路径 此方案为测试环境单机部署方案 服务端一旦挂了就不可用了 生产环境
  • Windows Server 2016-Windows 时间服务概览

    同步 Windows 时间服务 W32Time 的日期和时间的所有运行 Active Directory 域服务 AD DS 的计算机 时间同步至关重要的许多 Windows 服务和的业务线 LOB 应用正常运行 Windows 时间服务使
  • 获取请求地址路径参数

    package gacl request study import java io IOException import java io PrintWriter import javax servlet ServletException i
  • 华为OD机试 - 非严格递增连续数字序列(Java)

    题目描述 输入一个字符串仅包含大小写字母和数字 求字符串中包含的最长的非严格递增连续数字序列的长度 比如12234属于非严格递增连续数字序列 输入描述 输入一个字符串仅包含大小写字母和数字 输入的字符串最大不超过255个字符 输出描述 最长
  • CleanMyMac X 4.13.4许可证激活码2023最新免费版

    小伙伴们 你们好 今天兔八哥爱分享来聊聊cleanmymac X如何激活 关于cleanmymac的基本情况说明介绍的文章 网友们对这件事情都比较关注 那么现在就为大家来简单介绍下 希望对各位小伙伴们有所帮助 在不断更新的版本中 Clean
  • 论文p5解释 Bootstrap开关电路

    M7 M3这种箭头指回去的是P型 这是开关电路 也叫Bootstrap开关电路 所以分析的时候不用考虑是耗尽型或者增强型 只考虑高低电平打开和关闭开关 1 Clks是高电平时 详细分析图如下 最终目的是Cs上极板接到Vdd 下极板接地 于是
  • 如何用Python进行大数据挖掘和分析

    互联网创业离不开数据 如果能自己做个数据爬虫 那岂不是一件很美好的事情吗 其实自己做数据挖掘不是梦 学点Python的基本功能 5步就能让你成为一个爬虫高手
  • 影响力六大原则讲解

    文章目录 写在前面 互惠原则 承诺一致原则 社会认同 喜好 权威 稀缺 写在前面 人虽然是万物之灵 但还是有很多类似动物的本能 受很多其他因素的影响很多时候就像膝跳反射一样本能的发生着 影响着我们做的一个选择 正所谓 一言不合就XX 我们这
  • 【CSDN铁粉】CSDN铁粉增长终极奥义之如何快速破千铁粉

    文章目录 写在前面 涉及知识 1 铁粉是个啥 2 铁粉如何增长 1 持续的优质创作 2 与粉丝周期互动 3 参加活动与比赛 3 铁粉对您的意义 总结 写在前面 其实我也是下午去看那个6月份的城市获奖的名单时候 无意的去看了一下CSDN官方博
  • QuotaExceededError The quota has been exceeded

    一 前言 我首先描述下 这种报错出现的场景 ios lt 10 真机 Safari 的无痕浏览模式 使用localStorage or sessionStorage 的 setItem 当然 问题肯定社区有解决方案 以下链接可以满足你想要的
  • 2019第十届蓝桥杯【c++省赛B组】第九题

    第九题 后缀表达式 标题 后缀表达式 时间限制 1 0s 内存限制 256 0MB 本题总分 25 分 时间限制 1 0s 内存限制 256 0MB 本题总分 25 分 问题描述 给定 N 个加号 M 个减号以及 N M 1 个整数 A 1
  • STM32 CAN通信协议详解—小白入门(二)

    文章目录 一 CAN通信协议简介 二 CAN物理层 2 1 闭环总线网络 2 2 开环总线网络 2 3 通信节点 2 4 差分信号 2 5 CAN协议的差分信号 三 协议层 3 1 CAN的波特率及位同步 3 2 位时序分解 3 3 通讯的
  • Stm32学习笔记,3万字超详细

    Stm32学习笔记 文章目录 Stm32学习笔记 前言的前言 前言 笔记 Stm32 三种开发方式的区别 为什么Stm32初始化外设都需要先打开时钟 GPIO八种模式 Stm32寄存器映射 Stm32中的位段映射 Stm32中的时钟系统 S