文章的原标题是【Stm32学习笔记】,但是在这个浮躁的时代,不当个标题狗是不会有人点进来的。而既然是发布出来了,那肯定是想要别人点个赞,点个收藏关注一下的,所以在发布的时候还是换了一个浮夸点的标题了。
本文章主要记录本人在学习stm32过程中的笔记,也插入了不少的例程代码,方便到时候CV。绝大多数内容为本人手写,小部分来自stm32官方的中文参考手册以及网上其他文章;代码部分大多来自江科大和正点原子的例程,注释是我自己添加;配图来自江科大/正点原子/中文参考手册。
笔记内容都是平时自己一点点添加,不知不觉都已经这么长了。其实每一个标题其实都可以发一篇,但是这样搞太琐碎了,所以还是就这样吧。
喜欢的话,就点赞收藏关注一下~
本人技术有限,如有错误,欢迎在评论区或者私信指点。
本笔记内容以 Stm32F103xx 型号为研究对象。
说明:运行速度,性能损失的问题,都只是相对问题,实际上大多数情况下都可以忽略。
开启外设时钟的方法:
/*
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_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讲,将 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;
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 有5个时钟源:HSI、 HSE、LSI、LSE、PLL
中文名称 | 解释 | |
---|---|---|
HSI | 高速内部时钟 | RC振荡器,频率为8MHZ,精度不高。 |
HSE | 高速外部时钟 | 可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHZ~16MHz。 |
LSI | 低速内部时钟 | RC振荡器,频率为40kHz,提供低功耗时钟。一般用于看门狗(WDG) 担当一个低功耗时钟源的角色,它可以在停机和待机模式下保持运行,为独立看门狗和 自动唤醒单元提供时钟。 |
LSE | 低速外部时钟 | 接频率为32.768kHz的石英晶体。一般用于实时时钟(RTC) |
PLL | 锁相环倍频输出 | 本质上与其他四个时钟源不一样,这个时钟源是将 时钟输入源 进行 倍频 再输出 时钟输入源可选择为 HSI / 2 、HSE 或 HSE / 2 。倍频可选择为2~16倍,但是其输出频率最大不得超过72MHZ。 倍频器的原理:https://www.bilibili.com/video/BV1Mq4y1G77m |
时钟安全系统(CSS)
Stm32中还有一个时钟安全系统(CSS),在出现意外情况下还挺有用的。不过既然说是意外,就说明出现的概率并不大,因此这个功能没有什么存在感。
时钟安全系统可以通过软件被激活。一旦其被激活,时钟监测器将在HSE振荡器启动延迟后被使能,并在HSE时钟关闭后关闭。
如果HSE时钟发生故障,HSE振荡器将被自动关闭,时钟失效事件将被送到高级定时器(TIM1和 TIM8)的刹车输入端,并产生时钟安全中断CSSI,允许软件完成营救操作。此CSSI中断连接到 Cortex?-M3的NMI中断(不可屏蔽中断)。
如果HSE振荡器被直接或间接地作为系统时钟,(间接的意思是:它被作为PLL输入时钟,并且 PLL时钟被作为系统时钟),时钟故障将导致系统时钟自动切换到HSI振荡器,同时外部HSE振荡 器被关闭。在时钟失效时,如果HSE振荡器时钟(被分频或未被分频)是用作系统时钟的PLL的输 入时钟,PLL也将被关闭。
——Stm32F10xxx参考手册(中文).pdf
AHB,是Advanced High performance Bus的缩写,高级高性能总线;
APB,是Advanced Peripheral Bus的缩写,高级外设总线。
从图中就可以看出,APB1、APB2都是AHB系统总线进行桥接出来的。另外APB1最高只有36MHz,APB2最高可以达到72MHz。
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时钟
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); //初始化中断通道
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的读操作可以将该位清零。
*/
}
}
/*
配置外部中断的示例代码
*/
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);
}
}
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_GetFlagStatus
只是纯粹读取中断标志位的状态,但是实际上这并不准确,因为设置 EXTI_IMR 寄存器可以对该中断进行屏蔽;而 EXTI_GetITStatus
除了读取中断标志位,还查看 EXTI_IMR 寄存器是否对该中断进行屏蔽。
另外,EXTI_ClearFlag
和 EXTI_ClearITPendingBit
则是什么区别都没有,内部代码完全一样。
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 (备份域软件复位)
不处于备份域,因为没有必要。
备份寄存器拥有以下特性
备份寄存器的复位
后备区域的保护
在复位之后,对 后备区域(备份寄存器和RTC)
的访问将被禁止,后备区域被保护以防止可能存在的意外的写操作。
需要执行以下操作可以使能对后备区域的访问。
电源控制 (PWR)
与 备份寄存器 (BKP)
的时钟/*
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,写之前要取消写保护才可以
RTC的本质与定时器类似,就是一个计数器,每秒加一让其可以实现更新时间。
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寄存器。
配置过程:
仅当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();
}
Stm32F10xxx有三种低功耗模式:
WFI:等待中断,如果执行WFI指令进入睡眠模式,任意一个被嵌套向量中断控制器响应的外设中断都能将系统从 睡眠模式唤醒
WFE:等待事件,如果执行WFE指令进入睡眠模式,则一旦发生唤醒事件时,微处理器都将从睡眠模式退出
除了进行低功耗模式外,还可以在正常运行时使用下面方法降低功耗:
睡眠模式:
在睡眠模式下,仅停止CPU运作,对于其他外设,将保持原本进入睡眠模式的状态。
有两种选项可用于选择睡眠模式进入机制
区别就是在于是否处理完当前的中断再进入睡眠,因为一般来说,中断具有很高的实时性,不应该在中断中途进入睡眠。
停止模式:
在停止模式下,除了SRAM(内存)和寄存器内容被保留下来外,其他时钟将会被停止,所有的I/O引脚都保持它们在运行模式时的状态。另外,
进入停止模式需要等待闪存编程与APB访问完成,不然会等待完成再进入。
当一个中断或唤醒事件导致退出停止模式时,HSI RC振荡器将被选为系统时钟。
为了进入停止模式,所有的外部中断的请求位(挂起寄存器(EXTI_PR))和RTC的闹钟标志都必须被清除,否则停止模式的进入流程将会被跳过,程序继续运行。
说人话就是要把中断标志清除,不然刚进入停止模式就会被唤醒,相对于没进
进入停止模式可以配置以下外设正常运行:
独立看门狗(IWDG):可通过写入看门狗的键寄存器或硬件选择来启动IWDG。一旦启动了独立看门狗,除了系统复位,它不能再被停止
实时时钟(RTC):通过备份域控制寄存器 (RCC_BDCR)的RTCEN位来设置
内部RC振荡器(LSI RC):通过控制/状态寄存器 (RCC_CSR)的LSION位来设置
外部32.768kHz振荡器(LSE):通过备份域控制寄存器 (RCC_BDCR)的LSEON位设置
ADC与DAC:如果在进入该模式前ADC和DAC没有被关闭,那么这些外设仍然消耗电流。通过设置寄存器ADC_CR2 的 ADON 位和寄存器 DAC_CR 的 ENx 位为0可关闭这2个外设
电压调节器:可以通过配置电源控制寄存器(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闹钟事件将系统从停止模式下唤醒,必须进行如下操作:
/*
三种模式的进入代码示例
*/
/*进入睡眠模式*/
/*
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)模式
规则组:用于常规使用
注入组:用于突发情况使用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)
}
DMA 全程 Direct Memory Access (直接存储器存取),功能就是数据复制,优点就是能代替CPU负责数据复制,让CPU空出来处理其他任务。
另外,根据查资料得到,DMA的搬运速度没有CPU搬运的速度快的。详细可以看这里
数据复制方向支持:存储器到存储器、存储器到外设、外设到存储器。其中因为Flash一般为只读,所以存储器到存储器为 Flash到SRAM 、SRAM到SRAM。
数据宽度:
支持 字节(Byte,8位)、半字(HalfWord,16位)、字(Word,32位),支持不同宽度的数据复制,复制对齐为低位对齐。例如:半字(0x1122)复制到字节,则会把低八位复制过去,结果为0x22;半字(0x1122)复制字,则会把半字复制到字的低位,结果为0x00001122。
地址自增:
模式:正常模式(复制完就停下)、循环模式(复制完重新开始,循环模式不可用于存储器到存储器)
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有两种方案,一是软件模拟I2C,二是硬件I2C。两种方案各有各的优缺点,因此了解清楚才能选择适合的。
关于硬件I2C卡死问题具体可以看
总结一下Stm32的硬件I2C问题:
1.当时钟频率太高时容易出问题,出问题的概率和时钟频率成正比。
2.当存在中断会打断硬件IIC工作时(中断会导致),容易出现问题。
硬件I2C的发送流程图:
硬件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;
}
/*
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中的CAN架构:
设置
发送:
接收:
2个三级深度接收邮箱(FIFO):共可以接收6个报文
注:FIFO是英文First In First Out 的缩写,是一种先进先出的数据缓存器
锁定模式:锁定状态下,接收溢出则丢弃;非锁定状态下,接收溢出则覆盖
过滤器:
1个32位掩码模式
/2个32位标识符列表模式
/2个16位掩码模式
/4个16位标识符列表模式
测试模式图解:
过滤器:
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;
}