目录
MDK软件的使用
MDK编译结果里面的几个数据的意义:
MDK头文件包含
警告解决
CUBEMX使用注意事项
Debug问题
可分离模块文件生成配置问题
HAL库的理解
重要的文件:
HAL库特点
时钟系统
时钟树
时钟使能
时钟禁用
端口复用和重映射
MCO 输出
NVIC中断
GPIO模块
串口通信
外部中断
独立看门狗
窗口看门狗
普通定时器
普通定时器配置步骤:
PWM
捕获模块
FSMC
RTC时钟配置
PWR唤醒
配置步骤
ADC
配置过程
DAC
配置步骤
DMA
配置步骤:
IIC
配置过程
SPI
配置过程
CAN
配置过程:
前言:该文档整理来自正点原子与野火电子开源下载中心;之前接触过HAL库,但是不是很熟悉,现在整理了一下来复习HAL库的使用,文章设为转载,如有侵权,请联系我,我马上删除。
MDK软件的使用
MDK编译结果里面的几个数据的意义:
Code:表示程序所占用FLASH的大小(FLASH)。
RO-data:即Read Only-data,表示程序定义的常量(FLASH)。
RW-data:即Read Write-data,表示已被初始化的变量(SRAM)
ZI-data:即Zero Init-data,表示未被初始化的变量(SRAM)
MDK头文件包含
1、点击 project->New uVision Project;
2、设置相应的保存路径和保存工程名字(不要用中文);
3、芯片选型以及选择逐渐;
4、向工程文件拷贝需要的库;
5、目录树上点击右键Manage Project Items添加需要的文件;
6、设置头文件包含的路径(十分要注意,用点选,不要直接输入,不然可能没法真正包含进去,就会有缺失头文件的问题)。

7、设置中间文件的保存路径;
注意一下头文件包含的问题
"./":代表目前所在的目录
"../":代表上一层目录
以"/"开头:代表根目录
Windows的路径中用反单斜杠\
因为反单斜杠\在C/C++/C# 中是转义前导字符,例如 \n 代表换行。为了避免歧义,路径中的\必须用//。
警告解决
内容是:“warning: #1-D: last line of file ends without a newline”。我们只需要在main.c函数结尾加一个回车即可解决,这个是MDK自身的BUG。
CUBEMX使用注意事项
Debug问题
选择SYS里面的debug模式,如果用HAL库不进行SWD或JTAG配置,单片机只能进行下载一次程序,要进行第二次或更多次程序下载,需要按复位键(如果你的单片机有复位按键的话),或者用镊子夹住复位线路上的电容使其短路,点击Keil下载,再松开镊子。

可分离模块文件生成配置问题
记得要通过coder generator 中的选项勾选Generate peripheral initialization as a pair of '.c/.h' files per peripheral从而生成分离模块文件。
HAL库的理解
重要的文件:
stm32l1xx_hal_conf.h文件
这是hal库的配置文件。cubeMX软件在配置功能时,使用某一部分功能,会打开某一部分的功能接口,在stm32l1xx_hal_conf.h文件中有该部分的代码。
- #define HAL_MODULE_ENABLED
- #define HAL_ADC_MODULE_ENABLED
- /*#define HAL_CRYP_MODULE_ENABLED */
- /*#define HAL_COMP_MODULE_ENABLED */
- /*#define HAL_CRC_MODULE_ENABLED */
注意的是要用某些模块的话,这些标识符得定义才能将这个模块包含进去编译。否者会出现Undefined symbol的错误。一般做法是全部不注释。
stm32f1xx.h头文件
它是所有stm32f1系列的顶层头文件。使用STM32F1任何型号的芯片,都需要包含这个头文件。同时,因为stm32f1系列芯片型号非常多,ST为每种芯片型号定义了一个特有的片上外设访问层头文件,比如STM32F103系列,ST定义了一个头文件stm32f103xx.h,然后stm32f1xx.h顶层头文件会根据工程芯片型号,来选择包含对应芯片的片上外设访问层头文件。
在C/C++选项卡里面输入的全局宏定义了标识符STM32F103xx。使得头文件stm32f103xx.h会被整个工程所引用。
system_stm32f1xx.c/system_stm32f1xx.h文件
头文件system_stm32f1xx.h和源文件system_stm32f1xx.c主要是声明和定义了系统初始化函数SystemInit以及系统时钟更新函数SystemCoreClockUpdate。SystemInit函数的作用是进行时钟系统的一些初始化操作以及中断向量表偏移地址设置,但它并没有设置具体的时钟值。也就是说我们要根据自己的需求编写void SystemClock_Config(void);来实现始终具体值的配置
在启动文件startup_stm32f103xx.s中会设置系统复位后,直接调用SystemInit函数进行系统初始化。SystemCoreClockUpdate函数是在系统时钟配置进行修改后,调用这个函数来更新全局变量SystemCoreClock的值,变量SystemCoreClock是一个全局变量,开放这个变量可以方便我们在用户代码中直接使用这个变量来进行一些时钟运算。
stm32f1xx_hal_msp.c文件
MSP,全称为MCU support package,函数名字中带有MspInit的函数,它作用是进行MCU级别硬件初始化设置,并且它们通常会被上一层的初始化函数所调用,目的是为了把MCU相关的硬件初始化剥夺出来,方便用户代码在不同型号的MCU上移植。
stm32f1xx_hal_msp.c文件定义了两个函数HAL_MspInit和HAL_MspDeInit。这两个函数分别被文件stm32f1xx_hal.c中的HAL_Init和HAL_DeInit所调用。HAL_MspInit函数的主要作用是进行MCU相关的硬件初始化操作。
startup_stm32f103xe.s启动文件
STM32系列所有芯片工程都会有一个.s启动文件。对于不同型号的stm32芯片启动文件也是不一样的。
启动文件的作用主要是进行堆栈的初始化,中断向量表以及中断函数定义等。启动文件有一个很重要的作用就是系统复位后引导进入main函数。
stm32f1xx_it.c/stm32f1xx_it.h文件
stm32f1xx_it.h中主要是一些中断服务函数的申明。stm32f1xx_it.h中是这些中断服务函数定义。
Weak修饰符
加上了__weak修饰符的函数,用户可以在用户文件中重新定义一个同名函数,最终编译器编译的时候,会选择用户定义的函数,如果用户没有重新定义这个函数,那么编译器就会执行__weak声明的函数,并且编译器不会报错。
HAL库工作流程
1.Reset_Handler 引导进入SystemInit 函数和main函数(startupstm32f103xe.s)
2. SystemInit 函数进行系统初始化。(system_stm32f1XX.c)
3.进入main函数。(main.c)
4.HAL_Init函数初始化。(stm32f1xx_hal.c)
5.HAL_Init函数内部调用HAL_MspInit(用户编写)进行初始化。
6.外设HAL_XXX_Init函数初始化。(stm32f1xx_hal_XXX.c)
7.外设HAL_XXX_Init函数内部调用HAL_XXX_MspInit(用户编写)进行初始化。
8.用户编写逻辑代码。
注意:系统初始化之后的中断优先级分组组号的设置:默认情况下调用HAL初始化函数HAL_Init之后,会设置分组为组4 。
HAL库配置流程
基本结构
1.配置外设句柄,进行模块HAL_XXX_Init初始化操作。
2.编HAL_XXX_MspInit函数编写:编写的主要是时钟开启、引脚复用、中断响应等级配置、外设互连接等操作。
3.启动模块(与步骤1同一个函数XXX_Init()内编写)、或者启动模块中断等等;
4.编写中断函数,实现对应的回调函数
中断运作流程
- //1.中断处理
- void TIM5_IRQHandler(void)
- {
- HAL_TIM_IRQHandler(&htim5);
- }
- //2.清除标志,进入回调函数(多个句柄共用)
- void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim);
- //3.回调函数(多个句柄共用,因此需要判断)
- void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
- {
- if(TIM5 == htim->Instance)//需要特别处理
- {
- ///....
- }
- }
清除引脚标志位的话,用cubeMX中的一般不用,因为HAL_xxx_IRQHandler中会清除相关标志再进入中断的回调函数。(原子做法)
要注意的是由于这个是清除标志统一处理,所以if嵌套得很多层,效率很低,所以我们一般重写xxx_IRQHandler(),(十分要注意这里要手动清除标志位),使其进入回调函数,然后再重写回调函数。(野火的做法)。
标识符简介
- __HAL_PPP_ENABLE_IT(HANDLE, INTERRUPT): //使能一个指定的外设中断
- __HAL_PPP_DISABLE_IT(HANDLE, INTERRUPT)://失能一个指定的外设中断
- __HAL_PPP_GET_IT (HANDLE, __ INTERRUPT __)://获得一个指定的外设中断状态
- __HAL_PPP_CLEAR_IT (HANDLE, __ INTERRUPT __)://清除一个指定的外设的中断状态
- __HAL_PPP_GET_FLAG (HANDLE, FLAG)://获取一个指定的外设的标志状态
- __HAL_PPP_CLEAR_FLAG (HANDLE, FLAG)://清除一个指定的外设的标志状态
- __HAL_PPP_ENABLE(HANDLE) ://使能外设__HAL_PPP_DISABLE(HANDLE) :失能外设
- __HAL_PPP_XXXX (HANDLE, PARAM) ://指定外设的宏定义
- _HAL_PPP_GET IT_SOURCE (HANDLE, __ INTERRUPT __)://检查中断源
HAL库特点
外设句柄定义
用户代码的第一大部分:对于外设句柄的处理。HAL库在结构上,对每个外设抽象成了一个称为ppp_HandleTypeDef的结构体,其中ppp就是每个外设的名字。*所有的函数都是工作在ppp_HandleTypeDef指针之下。
1. 多实例支持:每个外设/模块实例都有自己的句柄。
2. 外围进程相互通信:该句柄用于管理进程例程之间的共享数据资源。
三种编程方式
HAL库对所有的函数模型也进行了统一。在HAL库中,支持三种编程模式:轮询模式、中断模式、DMA模式(如果外设支持)。
三大回调函数
1. 外设系统级初始化/解除初始化回调函数(用户代码的第二大部分:对于MSP的处理):
HAL_PPP_MspInit()和 HAL_PPP_MspDeInit();
2.处理完成回调函数:HAL_PPP_ProcessCpltCallback*(Process指具体某种处理);
3.错误处理回调函数:HAL_PPP_ErrorCallback();
时钟系统
时钟树
在STM32中,有五个时钟源,为HSI、HSE、LSI、LSE、PLL。从时钟频率来分可以分为高速时钟源和低速时钟源,在这5个中HIS,HSE以及PLL是高速时钟,LSI和LSE是低速时钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中HSE和LSE是外部时钟源,其他的是内部时钟源。(external)
①、HSI是高速内部时钟,RC振荡器,频率为8MHz。
②、HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz。我们的开发板接的是8M的晶振。
③、LSI是低速内部时钟,RC振荡器,频率为40kHz。独立看门狗的时钟源只能是LSI,同时LSI还可以作为RTC的时钟源。
④、LSE是低速外部时钟,接频率为32.768kHz的石英晶体。这个主要是RTC的时钟源。
⑤、PLL为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为2~16倍,但是其输出频率最大不得超过72MHz
系统时钟SYSCLK:系统时钟来源可以是:HSI、PLLCLK、HSE
系统时钟SYSCLK 经过AHB 预分频器分频之后得到时钟叫APB 总线时钟,即HCLK。
APB1 总线时钟PCLK1 由HCLK 经过低速APB 预分频器得到,分频因子可以是:[1,2,4,8,16],APB1上面连接的是低速外设,包括电源接口、备份接口、CAN、USB、I2C1、I2C2、UART2、UART3等等。
APB2 总线时钟PCLK2 由HCLK 经过高速APB2 预分频器得到,分频因子可以是:[1,2,4,8,16]APB2上面连接的是高速外设包括UART1、SPI1、Timer1、ADC1、ADC2、所有普通IO口(PA~PE)、第二功能IO口等。
SystemInit主要做了如下三个方面工作:
1) 复位RCC时钟配置为默认复位值(默认开始了HIS)
2) 外部存储器配置
3) 中断向量表地址配置
HAL库的SystemInit函数除了打开HSI之外,没有任何时钟相关配置,所以使用HAL库我们必须编写自己的时钟配置函数。
void SystemClock_Config(void);
- void SystemClock_Config(void)
- {
- RCC_OscInitTypeDef RCC_OscInitStruct = {0};
- RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
- RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;//时钟源为 HSE
- RCC_OscInitStruct.HSEState = RCC_HSE_ON; //打开 HSE
- RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; //HSE预分频
- RCC_OscInitStruct.HSIState = RCC_HSI_ON; //打开 PLL
- RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; //PLL时钟源选择 HSE
- RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; //主 PLL倍频因子
- RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
- if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) //rcc震荡源初始化
- {
- Error_Handler();
- }
- //选中 PLL作为系统时钟源并且配置 HCLK,PCLK1和 PCLK2
- RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
- |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
- //设置系统时钟时钟源为 PLL
- RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
- RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; //AHB分频系数为 1
- RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; //APB1分频系数为 2
- RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; //APB2分频系数为 1
- //同时设置 FLASH延时周期为 2WS,也就是 3个 CPU周期。
- if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
- {
- Error_Handler();
- }
- }
时钟使能
如果在使用外设之前没有使能外设时钟,这个外设是不可能正常运行的。
- __HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIOA时钟
- __HAL_RCC_DMA1_CLK_ENABLE();//使能DMA1时钟
- __HAL_RCC_USART2_CLK_ENABLE();//使能串口2时钟
- __HAL_RCC_TIM1_CLK_ENABLE();//使能TIM1时钟
本质:改变位的值(通过宏定义以及一些标识符);
时钟禁用
- __HAL_RCC_DMA1_CLK_DISABLE();//禁止DMA1时钟
- __HAL_RCC_USART2_CLK_DISABLE();//禁止串口2时钟
- __HAL_RCC_TIM1_CLK_DISABLE();//禁止TIM1时钟
端口复用和重映射
- GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出
MCO 输出
在STM32F103 系列中,PA8 可以复用为MCO 引脚,对外提供时钟输出,用示波器监控该引脚的输出来判断我们的系统时钟是否设置正确。
- void HAL_RCC_MCOConfig(uint32_t RCC_MCOx, uint32_t RCC_MCOSource, uint32_t RCC_MCODiv)
NVIC中断
STM32有84个中断,包括16个内核中断和68个可屏蔽中断,具有16级可编程的中断优先级。而我们常用的就是这68个可屏蔽中断,但是STM32的68个可屏蔽中断,在STM32F103系列上面,又只有60个(在107系列才有68个)。
这里需要注意两点:第一,如果两个中断的抢占优先级和响应优先级都是一样的话,则看哪个中断先发生就先执行;第二,高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的。而抢占优先级相同的中断,高优先级的响应优先级不可以打断低响应优先级的中断。
组别设置
- void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);
这个函数的作用是对中断的优先级进行分组,这个函数在系统中只需要被调用一次,一旦分组确定就最好不要更改,否则容易造成程序分组混乱。例子
- HAL_NVIC_SetPriorityGrouping (NVIC_PriorityGroup_2);//确定了中断优先级分组为2,也就是 2位抢占优先级 ,2位响应优先级,抢占优先级和响应优先级的值的范围均为 0 -3。
使能
- void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);
- void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);
- void HAL_NVIC_DisableIRQ(IRQn_Type IRQn);
- //第一个函数HAL_NVIC_SetPriority是用来设置单个优先级的抢占优先级和响应优先级的值。
- //第二个函数HAL_NVIC_EnableIRQ是用来使能某个中断通道。
- //第三个函数HAL_NVIC_DisableIRQ是用来清除某个中断使能的,也就是中断失能。
- //这部分代码可以通过cubeMX 配置自动生成
- HAL_NVIC_SetPriority(TIM1_UP_IRQn, 1, 0);
- HAL_NVIC_EnableIRQ(TIM1_UP_IRQn);
- HAL_NVIC_SetPriority(TIM1_CC_IRQn, 2, 0);
- HAL_NVIC_EnableIRQ(TIM1_CC_IRQn);
GPIO模块
STM32的IO口可以由软件配置成如下8种模式:
1、输入浮空
2、输入上拉
3、输入下拉
4、模拟输入
5、开漏输出
6、推挽输出
7、推挽式复用功能
8、开漏复用功能
输入上拉与下拉的区别:引脚的两个保护二级管可以防止引脚外部过高或过低的电压输入,当引脚电压高于VDD 时,上方的二极管导通,当引脚电压低于VSS 时,下方的二极管导通,防止不正常电压引入芯片导致芯片烧毁。
推挽输出模式:线路经过一个由P-MOS 和N-MOS 管组成的单元电路。在该结构中输入高电平时,经过反向后,上方的P-MOS 导通,下方的N-MOS 关闭,对外输出高电平;而在该结构中输入低电平时,经过反向后,N-MOS 管导通,P-MOS 关闭,对外输出低电平。当引脚高低电平切换时,两个管子轮流导通,P 管负责灌电流,N 管负责拉电流,使其负载能力和开关速度都比普通的方式有很大的提高。
开漏输出模式:上方的P-MOS 管完全不工作。如果我们控制输出为0,低电平,则P-MOS 管关闭,N-MOS 管导通,使输出接地,若控制输出为1 (它无法直接输出高电平)时,则P-MOS 管和N-MOS 管都关闭,所以引脚既不输出高电平,也不输出低电平,为高阻态。。为正常使用时必须外部接上拉电阻。
推挽输出模式一般应用在输出电平为0 和3.3 伏而且需要高速切换开关状态的场合。
开漏输出一般应用在I2C、SMBUS 通讯等需要“线与”功能的总线电路中。除此之外,还用在电平不匹配的场合。
模拟输入:当GPIO 引脚用于ADC 采集电压的输入通道时,用作“模拟输入”功能,此时信号是不经过施密特触发器的,因为经过施密特触发器后信号只有0、1 两种状态,所以ADC 外设要采集到原始的模拟信号,信号源输入必须在施密特触发器之前。类似地,当GPIO 引脚用于DAC 作为模拟电压输出通道时,此时作为“模拟输出”功能,DAC 的模拟信号输出就不经过双MOS 管结构,模拟信号直接输出到引脚。
在HAL库开发中,初始化GPIO是通过GPIO初始化函数完成:
- void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
这个函数有两个参数,第一个参数是用来指定需要初始化的GPIO对应的GPIO组,取值范围为GPIOA~GPIOE。第二个参数为初始化参数结构体指针,结构体类型为GPIO_InitTypeDef。
通过初始化结构体初始化GPIO的常用格式是:
- GPIO_Initure.Pin=GPIO_PIN_9|GPIO_PIN_10; //PF9,10
- GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
- GPIO_Initure.Pull=GPIO_PULLUP; //上拉
- GPIO_Initure.Speed= GPIO_SPEED_FREQ_HIGH; //高速
- HAL_GPIO_Init(GPIOF,&GPIO_Initure);
寄存器的值来控制 IO口的输出状态是通过函数
- void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);
- //使用实例如下:
- HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_SET);
读取电平
- GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
- //函数是用来读取一组IO口的一个输入电平。
- HAL_GPIO_ReadPin (GPIOF, GPIO_Pin_5);
串口通信
1) 串口参数初始化(波特率/停止位等),并使能串口。
串口初始化函数HAL_UART_Init:
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);
该函数只有一个入口参数
- USART_HandleTypeDef husart1;
- husart1.Instance = USART1; //USART1
- husart1.Init.BaudRate = 115200; //波特率
- husart1.Init.WordLength = USART_WORDLENGTH_8B;//字长为 8位格式
- husart1.Init.StopBits = USART_STOPBITS_1; //一个停止位
- husart1.Init.Parity = USART_PARITY_NONE; //无奇偶校验位
- husart1.Init.Mode = USART_MODE_TX_RX; //收发模式
- husart1.Init.CLKPolarity = USART_POLARITY_LOW;
- husart1.Init.CLKPhase = USART_PHASE_1EDGE;
- husart1.Init.CLKLastBit = USART_LASTBIT_DISABLE;
- if (HAL_USART_Init(&husart1) != HAL_OK) //使能 USART1
- {
- Error_Handler();
- }
HAL库也提供了具体的串口使能和关闭方法,具体使用方法如下:
- __HAL_UART_ENABLE(handler); //使能句柄handler指定的串口
- __HAL_UART_DISABLE(handler); //关闭句柄handler指定的串口
串口作为一个重要外设,在调用的初始化函数HAL_UART_Init内部,会先调用MSP初始化回调函数进行MCU相关的初始化,函数为:
- void HAL_UART_MspInit(UART_HandleTypeDef *huart);
2)使能串口和GPIO口时钟
使用串口,所使能串口时钟和使用到的GPIO口时钟。例如使用串口1,使能串口1时钟和GPIOA时钟(串口1使用的是PA9和PA10)。方法如下:
- __HAL_RCC_USART1_CLK_ENABLE(); //使能USART1时钟
- __HAL_RCC_GPIOA_CLK_ENABLE(); //使能GPIOA时钟
3)GPIO口初始化设置(速度,上下拉等)以及复用映射配置
配置IO口为复用,同时复用映射到串口1。
- GPIO_Initure.Pin=GPIO_PIN_9; //PA9
- GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出
- GPIO_Initure.Pull=GPIO_PULLUP; //上拉
- GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH; //高速
- HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA9
- GPIO_Initure.Pin=GPIO_PIN_10; //PA10
- GPIO_Initure.Mode=GPIO_MODE_AF_INPUT; //模式要设置为复用输入模式!
- HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA10
4) 开启串口相关中断,配置串口中断优先级
使能串口中断的标识符__HAL_UART_ENABLE_IT(很关键,cubeMX不会帮我们自动开启)
- __HAL_UART_ENABLE_IT(huart,UART_IT_RXNE); //开启接收 完成 中断
- __HAL_UART_DISABLE_IT(huart,UART_IT_RXNE); //关闭 接收 完成 中断
- //第一个参数为串口句柄
- //第二个参数为我们要开启的中断类型值
- //对于中断优先级配置
- HAL_NVIC_EnableIRQ(USART1_IRQn); //使能 USART1中断通道
- HAL_NVIC_SetPriority(USART1_IRQn,3,3); //抢占优先级 3,子优先级 3
同样的还有另外一种同时开启模块和开启中断的方式
- //另外一种方式开启中断
- HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
- HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
回调函数
- //HAL库一共提供了 5个中断处理回调函数:
- void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);//发送完成回调函数
- void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);//发送完成过半
- void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//接收完成回调函数
- void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);//接收完成过半
- void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);//错误处理回调函数
中断服务函数为:
- void USART1_IRQHandler(void) ;
- //调用完再调用回调函数
串口数据接收和发送
- //HAL库操作 USART_DR寄存器发送数 据的函数:
- HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
- //HAL库操作 USART_DR寄存器读取串口接收到的数据的函数:
- HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
外部中断
STM32F103的中断控制器支持19个外部中断/事件请求。每个中断设有状态位,每个中断/事件都有独立的触发和屏蔽设置。
EXTI线0~15:对应外部IO口的输入中断。
EXTI线16:连接到PVD输出。
EXTI线17:连接到RTC闹钟事件。
EXTI线18:连接到USB唤醒事件。
EXTI线19:连接到以太网唤醒事件。
配置步骤
1) 设置IO口模式,触发条件,开启SYSCFG时钟,设置IO口与中断线的映射关系。
GPIO_Initure.Mode=GPIO_MODE_IT_RISING; //外部中断,上升沿触发
2) 使能IO口时钟,初始化IO口为输入
3) 开启串口相关中断,配置外部中断优先级
4) 编写中断服务函数。
- void EXTI0_IRQHandler();
- void EXTI1_IRQHandler();
- void EXTI2_IRQHandler();
- void EXTI3_IRQHandler();
- void EXTI4_IRQHandler();
- void EXTI9_5_IRQHandler();
- void EXTI15_10_IRQHandler();
5)编写中断处理回调函数HAL_GPIO_EXTI_Callback
- //判断的是引脚
- HAL_GPIO_EXTI_Callback(GPIO_Pin);
独立看门狗
STM32的独立看门狗由内部专门的40Khz低速时钟驱动,即使主时钟发生故障,它也仍然有效。
1)取消寄存器写保护(向IWDG_KR写入0X5555)
首先我们必须取消IWDG_PR和IWDG_RLR寄存器的写保护,这样才可以设置寄存器IWDG_PR和IWDG_RLR的值。取消写保护和设置预分频系数以及重装载值在HAL库中是通过函数HAL_IWDG_Init实现的。该函数声明为:
- HAL_StatusTypeDef HAL_IWDG_Init(IWDG_HandleTypeDef *hiwdg);
HAL_IWDG_Init函数使用的一般方法为:
- hiwdg.Instance = IWDG;
- hiwdg.Init.Prescaler = IWDG_PRESCALER_16;
- hiwdg.Init.Reload = 4095;
- if (HAL_IWDG_Init(&hiwdg) != HAL_OK)
- {
- while(1);
- }
2)重载计数值喂狗(向IWDG_KR写入0XAAAA)
在HAL中重载计数值的函数是HAL_IWDG_Refresh,该函数声明为:
- HAL_StatusTypeDef HAL_IWDG_Refresh(IWDG_HandleTypeDef *hiwdg);
3) 启动看门狗(向IWDG_KR写入0XCCCC)
HAL库函数里面启动独立看门狗的函数是__HAL_IWDG_START:
窗口看门狗
1)设置窗口值,分频数和计数器初始值
在HAL库中,这三个值都是通过函数HAL_WWDG_Init来设置的。该函数声明如下:
- HAL_StatusTypeDef HAL_WWDG_Init(WWDG_HandleTypeDef *hwwdg);
该函数只有一个入口参数,就是WWDG_HandleTypeDef结构体类型指针变量。该结构体有3三个成员变量,分别用来设置WWDG的预分频系数,窗口之以及计数器值。
- hwwdg.Instance = WWDG;
- hwwdg.Init.Prescaler = WWDG_PRESCALER_4; //设置分频系数
- hwwdg.Init.Window = 64; //设置窗口值
- hwwdg.Init.Counter = 64; //设置计数器值
- hwwdg.Init.EWIMode = WWDG_EWI_ENABLE; //使能窗口看门狗提前唤醒中断
- if (HAL_WWDG_Init(&hwwdg) != HAL_OK)
- {
- while(1);
- }
2)使能WWDG时钟
WWDG不同于IWDG,IWDG有自己独立的40Khz时钟,不存在使能问题。而WWDG使用的是PCLK1的时钟,需要先使能时钟。方法是:
- __HAL_RCC_WWDG_CLK_ENABLE(); //使能窗口看门狗时钟
3)使能中断通道并配置优先级(如果开启了WWDG
- HAL_NVIC_SetPriority(WWDG_IRQn,2,3); //抢占优先级 2,子优先级为 3
- HAL_NVIC_EnableIRQ(WWDG_IRQn); //使能窗口看门狗中断
4)窗口看门狗中断服务(在中断服务函数里面要将状态寄存器的EWIF位清空。)函数为:
- void WWDG_IRQHandler(void);
在HAL库中,喂狗函数为:
- HAL_StatusTypeDef HAL_WWDG_Refresh(WWDG_HandleTypeDef *hwwdg);
WWDG的喂狗操作实际就是往CR寄存器重写计数器值。
5)重写窗口看门狗唤醒中断处理回调函数HAL_WWDG_WakeupCallback
- void HAL_WWDG_EarlyWakeupCallback (WWDG_HandleTypeDef* hwwdg);
普通定时器
普通定时器配置步骤:
1)初始化定时器参数,设置自动重装值,分频系数,计数方式等。
在HAL库中,定时器的初始化参数是通过定时器初始化函数HAL_TIM_Base_Init实现的:
- HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim);
-
- //相关配置如下
- htim3.Instance = TIM3;
- htim3.Init.Prescaler = 72-1; //分频系数
- htim3.Init.CounterMode = TIM_COUNTERMODE_UP; //向上计数器
- htim3.Init.Period = 10000; //自动装载值
- htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;/时钟分频因子
- htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
- if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
- {
- while(1);
- }
2)TIM3时钟使能。
HAL中定时器使能是通过宏定义标识符来实现对相关寄存器操作的,方法如下:
- __HAL_RCC_TIM3_CLK_ENABLE(); //使能TIM3时钟
3)使能定时器更新中断,使能定时器
HAL库中,使能定时器更新中断和使能定时器两个操作可以在函数HAL_TIM_Base_Start_IT()中一次完成的,该函数声明如下:
- //注意此函数需要自己编写,cubeMX无法生成
- HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);
- //相应独立的调用
- __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);//使能句柄指定的定时器更新中断
- __HAL_TIM_DISABLE_IT (htim, TIM_IT_UPDATE);//关闭句柄指定的定时器更新中断
- __HAL_TIM_ENABLE(htim);//使能句柄 htim指定的定时器
- __HAL_TIM_DISABLE(htim);//关闭句柄 htim指定的定时器
一般情况下,与MCU有关的时钟使能,以及中断优先级配置我们都会放在该回调函数内部。函数声明如下:
- void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim);
4)中断服务
首先,中断服务函数是不变的,定时器3的中断服务函数为:
TIM3_IRQHandler();
一般情况下我们是在中断服务函数内部编写中断控制逻辑。但是HAL库定义了新的定时器中断共用处理函数HAL_TIM_IRQHandler,在每个定时器的中断服务函数内部,我们会调用该函数。该函数声明如下:
void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim);
而函数HAL_TIM_IRQHandler内部,会对相应的中断标志位进行详细判断,判断确定中断来源后,会自动清掉该中断标志位,同时调用不同类型中断的回调函数。所以我们的中断控制逻辑只用编写在中断回调函数中,并且中断回调函数中不需要清中断标志位。
- //常见的回调函数
- void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);//更新中断
- void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim);//输出比较
- void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim);//输入捕获
- void HAL_TIM_TriggerCallback(TIM_HandleTypeDef *htim);//触发中断
PWM
使用定时器的PWM输出功能时,HAL库为我们提供了一个独立的定时器初始化函数HAL_TIM_PWM_Init,该函数声明为:
- HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
使用HAL_TIM_PWM_Init初始化定时器时,回调函数为:HAL_TIM_PWM_MspInit,该函数声明为:
- void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim);
1)设置TIMx_CHx的PWM模式,输出比较极性,比较值等参数。
- HAL_StatusTypeDef HAL_TIM_PWM_ConfigChannel(TIM_HandleTypeDef *htim, TIM_OC_InitTypeDef* sConfig, uint32_t Channel);
相关配置如下:
- //oc(output compare)
- TIM_OC_InitTypeDef sConfigOC = {0};
- sConfigOC.OCMode = TIM_OCMODE_PWM1; //模式选择 PWM1
- sConfigOC.Pulse = 0; //设置比较值 ,此值用来确定占空比,
- sConfigOC.OCPolarity = TIM_OCPOLARITY_LOW; //输出比较极性为低
- sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
- if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2) != HAL_OK)
- {
- while(1);
- }
2)使能TIMx,使能TIMx的CHx输出。
该函数第二个入口参数Channel是用来设置要使能输出的通道号。
- HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
HAL库也同样提供了单独使能定时器的输出通道函数,函数为:
- void TIM_CCxChannelCmd(TIM_TypeDef* TIMx, uint32_t Channel, uint32_t ChannelState);
3)修改TIMx_CCRx来控制占空比。
- __HAL_TIM_GET_COMPARE(__HANDLE__, __CHANNEL__)//获取比较值
- __HAL_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) //设定比较值
实例如下:
- __HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_2,led0pwmval);
捕获模块
STM32F1的定时器,除了TIM6和TIM7,其他定时器都有输入捕获功能。STM32F1的输入捕获,简单的说就是通过检测TIMx_CHx上的边沿信号,在边沿信号发生跳变(比如上升沿/下降沿)的时候,将当前定时器的值(TIMx_CNT)存放到对应的通道的捕获/比较寄存器(TIMx_CCRx)里面,完成一次捕获。同时还可以配置捕获时是否触发中断
1)初始化TIMx,设置TIMx的ARR和PSC。(普通定时器操作)
当我们使用函数 HAL_TIM_IC_Init来初始化定时器的输入捕获功能时,该函数内部会调用输入捕获初始化回调函数 HAL_TIM_IC_MspInit来初始化与 MCU无关的步骤。
- HAL_StatusTypeDef HAL_TIM_IC_Init(TIM_HandleTypeDef *htim);
- void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim);
2)设置TIM5的输入捕获参数,开启输入捕获。
- HAL_StatusTypeDef HAL_TIM_IC_ConfigChannel(TIM_HandleTypeDef *htim, TIM_IC_InitTypeDef* sConfig, uint32_t Channel);
配置案例:
- TIM_IC_InitTypeDef sConfigIC = {0};
- sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;//上升沿
- sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; //IC1映射到 TI1上
- sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频
- sConfigIC.ICFilter = 8; //配置输入滤波器,不滤波
- if (HAL_TIM_IC_ConfigChannel(&htim5, &sConfigIC, TIM_CHANNEL_1) != HAL_OK)
- {
- while(1);
- }
3)使能捕获和更新中断(设置TIM5的DIER寄存器)
定时器的输入捕获功能:HAL提供了一个函数同时用来开启定时器的输入捕获通道和使能捕获中断,该函数为:
- HAL_StatusTypeDef HAL_TIM_IC_Start_IT (TIM_HandleTypeDef *htim, uint32_t Channel);
实际上该函数同时还使能了定时器,一个函数具备三个功能。
如果我们不需要开启捕获中断,只是开启输入捕获功能,HAL库函数为:
- HAL_StatusTypeDef HAL_TIM_IC_Start (TIM_HandleTypeDef *htim, uint32_t Channel);
4)开启TIMX时钟,配置PXX为复用功能(AF2),并开启下拉电阻。
5)使能定时器(设置TIM5的CR1寄存器)
HAL_TIM_IC_Start_IT 来开启输入捕获通道以及输入捕获中断,实际上它同时也开启了相应的定时器。单独的开启定时器的方法为:__HAL_TIM_ENABLE(); //开启定时器方法(与之前相同)
6)设置NVIC中断优先级
7)编写中断服务函数与回调函数
- void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);//更新(溢出 中断
- void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim);//捕获中断
8)设置输入捕获极性以及清除输入捕获极性设置方法。
- //函数法改变极性
- TIM_RESET_CAPTUREPOLARITY(&htim5, TIM_CHANNEL_1); //复位极性选择才能进行下行配置
- TIM_SET_CAPTUREPOLARITY(&htim5, TIM_CHANNEL_1, TIM_ICPOLARITY_RISING);//下次上升沿捕获
- //标识符法改变极性
- __HAL_TIM_SET_CAPTUREPOLARITY(&htim5, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
FSMC
FSMC,即灵活的静态存储控制器,能够与同步或异步存储器和16位PC存储器卡连接,STM32F1的FSMC接口支持包括SRAM、NAND FLASH、NOR FLASH和PSRAM等存储器。
1) 初始化FMC接口读写时序参数,初始化LCD(SRAM)控制接口
根据前面的讲解,我们把LCD当SRAM使用,连接在FMC接口之上,所以我们要初始化FMC读写时序参数以及LCD数据接口,也就是初始化三个寄存器FMC_BCRx,FMC_BTRx和FMC_BWTRx。HAL库提供了SRAM初始化函数HAL_SRAM_Init,该函数声明如下:
- HAL_StatusTypeDef HAL_SRAM_Init(SRAM_HandleTypeDef *hsram,FSMC_NORSRAM_TimingTypeDef *Timing, FSMC_NORSRAM_TimingTypeDef *ExtTiming;
相关配置如下:
- FSMC_NORSRAM_TimingTypeDef Timing = {0};
- FSMC_NORSRAM_TimingTypeDef ExtTiming = {0};
-
- hsram1.Instance = FSMC_NORSRAM_DEVICE;
- hsram1.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;
- /* hsram1.Init */
- hsram1.Init.NSBank = FSMC_NORSRAM_BANK4; //使用NE4
- hsram1.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE; //地址/数据线不复用
- hsram1.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM; //SRAM
- hsram1.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16; //16位数据宽度
- hsram1.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE; //是否使能突发访问,仅对同步突发存储器有效,此处未用到
- hsram1.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW;//等待信号的极性,仅在突发模式访问下有用
- hsram1.Init.WrapMode = FSMC_WRAP_MODE_DISABLE;
- hsram1.Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS; //存储器是在等待周期之前的一个时钟周期还是等待周期期间使能NWAIT
- hsram1.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE; //存储器写使能
- hsram1.Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE; //等待使能位,此处未用到
- hsram1.Init.ExtendedMode = FSMC_EXTENDED_MODE_ENABLE; //读写使用不同的时序
- hsram1.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE;//是否使能同步传输模式下的等待信号,此处未用到
- hsram1.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE; //禁止突发写
- /* Timing */
- Timing.AddressSetupTime = 0x06; //地址建立时间(ADDSET)为7个HCLK 13.8ns*7=96.6ns
- Timing.AddressHoldTime = 0;
- Timing.DataSetupTime = 26; //数据保存时间为27个HCLK =13.8*27=372.6ns
- Timing.BusTurnAroundDuration = 0;
- Timing.CLKDivision = 16;
- Timing.DataLatency = 17;
- Timing.AccessMode = FSMC_ACCESS_MODE_A; //模式A
- /* ExtTiming */
- ExtTiming.AddressSetupTime = 3; //地址建立时间(ADDSET)为4个HCLK =55.2ns
- ExtTiming.AddressHoldTime = 15; //数据保存时间为13.8ns*7个HCLK=96.6ns
- ExtTiming.DataSetupTime = 0x06;
- ExtTiming.BusTurnAroundDuration = 0; //总线周转阶段持续时间为0
- ExtTiming.CLKDivision = 16;
- ExtTiming.DataLatency = 17;
- ExtTiming.AccessMode = FSMC_ACCESS_MODE_A;//模式A
-
- if (HAL_SRAM_Init(&hsram1, &Timing, &ExtTiming) != HAL_OK)
- {
- while(1);
- }
2)存储区使能
实际上,当我们调用了存储器初始化函数之后,相应的使用到的存储区就已经被使能。SRAM存储区使能方法为:
- __FSMC_NORSRAM_ENABLE(hsram->Instance, hsram->Init.NSBank);
3) 使能FSMC和GPIO时钟,初始化IO口配置,设置映射关系
- __HAL_RCC_FSMC_CLK_ENABLE(); //使能FSMC时钟
FSMC接口支持多种存储器,包括SDRAM,NOR,NAND和PC CARD等。HAL库为每种支持的存储器类型都定义了一个独立的HAL库文件,并且在文件中定义了独立的初始化函数。同时还提供了独立的初始化函数。
- HAL_SDRAM_Init();//SDRAM初始化函数 ,省略入口参数
- HAL_NOR_Init();//NOR初始化函数 ,省略入口参数
- HAL_NAND_Init();//NAND初始化函数 ,省略入口参数
RTC时钟配置
STM32的实时时钟(RTC)是一个独立的定时器。STM32的RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。
RTC模块和时钟配置系统(RCC_BDCR寄存器)是在后备区域,即在系统复位或从待机模式唤醒后RTC的设置和时间维持不变。但是在系统复位后,会自动禁止访问后备寄存器和RTC,以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前,先要取消备份区域(BKP)写保护。
1)开启外部低速振荡器LSE,选择RTC时钟,并使能。
前面部分使RCC系统时钟配置,对于RTC:
RTC时钟源为函数为HAL_RCCEx_PeriphCLKConfig,使用方法为:
- PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
- PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
- if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
- {
- while(1);
- }
2)初始化RTC,设置RTC的分频,以及配置RTC参数
- //RTC初始化函数原型
- HAL_StatusTypeDef HAL_RTC_Init(RTC_HandleTypeDef *hrtc);
句柄中其中一个参数OutPut用来选择要连接到RTC_ALARM输出的标志,取值为:RTC_OUTPUT_DISABLE(禁止输出),RTC_OUTPUT_ALARMA(使能闹钟A输出),RTC_OUTPUT_ALARMB(使能闹钟B输出)和RTC_OUTPUT_WAKEUP(使能唤醒输出)。
- //句柄
- RTC_HandleTypeDef hrtc;
- //配置实例
- hrtc.Instance = RTC;
- hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND;
- hrtc.Init.OutPut = RTC_OUTPUTSOURCE_ALARM;
- if (HAL_RTC_Init(&hrtc) != HAL_OK)
- {
- while(1);
- }
3) 使能电源时钟和备份区域时钟;使能RTC时钟(MSP回调里实现):
- HAL_PWR_EnableBkUpAccess();//设置RTC唤醒前需要先执行备份区使能,并且保证备份区不会使能,否则低功耗后唤醒不了
- __HAL_RCC_BKP_CLK_ENABLE();使能电源时钟 PWR
- __HAL_RCC_RTC_ENABLE();//RTC时钟使能
-
4)相关中断配置与中断函数的编写
- //中断开启
- __HAL_RTC_ALARM_ENABLE_IT(&RTC_Handler, RTC_IT_SEC); //允许秒中断
- __HAL_RTC_ALARM_ENABLE_IT(&RTC_Handler, RTC_IT_ALRA); //允许闹钟中断
- //相关中断配置
- HAL_NVIC_SetPriority(RTC_IRQn, 3, 1);
- HAL_NVIC_EnableIRQ(RTC_IRQn);
- HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 3, 2);
- HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
相关中断函数如下:
- //统一处理
- void RTC_IRQHandler(void);
- //清除标记,进入回调
- void HAL_RTCEx_RTCIRQHandler(RTC_HandleTypeDef *hrtc);
- //回调
- HAL_RTCEx_RTCEventCallback(&hrtc);
- //闹钟的中断
- void RTC_Alarm_IRQHandler(void);
- void HAL_RTC_AlarmIRQHandler(RTC_HandleTypeDef *hrtc)
- void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
5)日期设置与时间设置
- //时间设置
- HAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format);
- //日期设置
- HAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format);
- //时间设置案例
- RTC_TimeTypeDef sTime = {0};
- sTime.Hours = 10;
- sTime.Minutes = 20;
- sTime.Seconds = 0;
-
- if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
- {
- while(1);
- }
- //日期设置案例
- RTC_DateTypeDef DateToUpdate = {0};
- DateToUpdate.WeekDay = RTC_WEEKDAY_SATURDAY;
- DateToUpdate.Month = RTC_MONTH_JULY;
- DateToUpdate.Date = 24;
- DateToUpdate.Year = 0;// Min_Data = 0 and Max_Data = 99
- if (HAL_RTC_SetDate(&hrtc, &DateToUpdate, RTC_FORMAT_BIN) != HAL_OK)
- {
- while(1);
- }
6)闹钟设置
- RTC_AlarmTypeDef sAlarm = {0};
- ///
- sAlarm.AlarmTime.Hours = 10;
- sAlarm.AlarmTime.Minutes = 30;
- sAlarm.AlarmTime.Seconds = 10;
- sAlarm.Alarm = RTC_ALARM_A;
- if (HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN) != HAL_OK)
- {
- while(1);
- }
7) 获取RTC当前日期和时间。
- //获取当前RTC时间的函数为:
- HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format);
- //获取当前RTC日期的函数为:
- HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format);
这两个函数非常简单,实际就是读取RTC_TR寄存器和RTC_DR寄存器的时间和日期的值,然后将值存放到相应的结构体中。
8)读备份区域和写备份区域寄存器
- uint32_t HAL_RTCEx_BKUPRead(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister);
- void HAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister, uint32_t Data);
在设置好时间之后,我们向RTC的BKR寄存器(地址0)写入标志字0X5050,用于标记时间已经被设置了。这样,再次发生复位的时候,该函数通过判断RTC对应BKR的值,来决定是不是需要重新设置时间,如果不需要设置,则跳过时间设置,这样不会重复设置时间,使得我们设置的时间不会因复位或者断电而丢失。
- if (HAL_RTCEx_BKUPRead(&RTC_Handler, RTC_BKP_DR1) != 0X5050) //是否第一次配置
- {
- //时间设置得稍微编写一下(因为hander.year范围0-99)
- RTC_Set(2021, 7, 24, 10, 56, 0); //设置日期和时间
- HAL_RTCEx_BKUPWrite(&RTC_Handler, RTC_BKP_DR1, 0X5050); //初始化完成标记
- }
PWR唤醒
配置步骤
1)配置PA0引脚为外部输入模式(类似外部中断)
2)使能PWR时钟。
在HAL库中,使能PWR时钟的方法是:
- __HAL_RCC_PWR_CLK_ENABLE(); //使能PWR时钟
3) 设置WK_UP引脚作为唤醒源。
使能时钟之后后再设置PWR_CSR的EWUP位,使能WK_UP用于将CPU从待机模式唤醒。在HAL库中,设置使能WK_UP用于唤醒CPU待机模式的函数是:
- HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); //设置WKUP用于唤醒
4)设置SLEEPDEEP位,设置PDDS位,执行WFI指令,进入待机模式。
在库函数中,进行上面三个功能进入待机模式是在函数HAL_PWR_EnterSTANDBYMode中实现的:
- void HAL_PWR_EnterSTANDBYMode(void);
5)配置中断响应优先级,编写WK_UP中断服务函数。
通过WK_UP中断(PA0中断)来唤醒CPU,要设置一下该中断函数,同时通过该中断函数里面进入待机模式。关于外部中断服务函数以及中断服务回调函数。
进入待机配置案例:
- __HAL_RCC_APB2_FORCE_RESET(); //复位所有IO口
- __HAL_RCC_PWR_CLK_ENABLE(); //使能PWR时钟
- __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); //清除Wake_UP标志
- HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); //设置WKUP用于唤醒
- HAL_PWR_EnterSTANDBYMode();
ADC
STM32的ADC是12位逐次逼近型的模拟数字转换器。它有18个通道,可测量16个外部和2个内部信号源。各通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以左对齐或右对齐方式存储在16位数据寄存器中。模拟看门狗特性允许应用程序检测输入电压是否超出用户定义的高/低阀值。
STM32的ADC最大的转换速率为1Mhz,也就是转换时间为1us(在ADCCLK=14M,采样周期为1.5个ADC时钟下得到),不要让ADC的时钟超过14M,否则将导致结果准确度下降。
STM32将ADC的转换分为2个通道组:规则通道组和注入通道组。规则通道相当于你正常运行的程序,而注入通道呢,就相当于中断。在你程序正常执行的时候,中断是可以打断你的执行的。同这个类似,注入通道的转换可以打断规则通道的转换,在注入通道被转换完成之后,规则通道才得以继续转换。
配置过程
1)初始化ADC,设置ADC时钟分频系数,分辨率,模式,扫描方式,对齐方式等信息。
初始化ADC是通过函数HAL_ADC_Init来实现的,该函数声明为:
- HAL_StatusTypeDef HAL_ADC_Init(ADC_HandleTypeDef* hadc);
配置实例
- //句柄
- ADC_HandleTypeDef hadc1;
- //配置
- hadc1.Instance = ADC1;
- hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;//扫描模式
- hadc1.Init.ContinuousConvMode = ENABLE;//开启连续转换模式或者单次转换模式
- hadc1.Init.DiscontinuousConvMode = DISABLE; 不连续采样模式
- hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;//软件开启,即触发源选取
- hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;//对齐方式:左对齐还是右对齐
- hadc1.Init.NbrOfConversion = 1; //规则序列中有多少个转换
- if (HAL_ADC_Init(&hadc1) != HAL_OK)
- {
- while(1);
- }
2)配置通道
设置规则序列1里面的通道,然后启动ADC转换。在转换结束后,读取转换结果值值。
设置规则序列通道以及采样周期的函数是:
- HAL_StatusTypeDef HAL_ADC_ConfigChannel(ADC_HandleTypeDef* hadc, ADC_ChannelConfTypeDef* sConfig);
通道配置实例:
- ADC_ChannelConfTypeDef sConfig = {0};
- //通道配置
- sConfig.Channel = ADC_CHANNEL_9; //通道9
- sConfig.Rank = ADC_REGULAR_RANK_1;//第 1个序列,序列
- sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;//采样时间
- if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) //通道配置
- {
- while(1);
- }
3)校准ADC
- HAL_ADCEx_Calibration_Start(&hadc1); //校准 ADC必须在ADC转换启动前或停止后
4)开启AD转换器。
与定时器类似的是,HAL库提供了多种方式开启ADC;
函数法开启ADC
- HAL_ADC_Start(&hadc1); //开启ADC 轮询模式
标识符法开启
- __HAL_ADC_ENABLE((&hadc1);//另外一种方式开启ADC
5)开启模拟引脚时钟和ADCx时钟,设置PXx为模拟输入。
要注意的是,用来做ADC 输入的IO 不能被复用,否则会导致采集到的信号不准确。
- //引脚时钟与ADC时钟
- __HAL_RCC_GPIOA_CLK_ENABLE();
- __HAL_RCC_ADC1_CLK_ENABLE(); //使能 ADC1时钟
- //引脚初始化
- GPIO_InitStruct.Pin = GPIO_PIN_1;
- GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
- HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
6)中断配置
数据转换结束后,可以产生中断,中断分为三种:规则通道转换结束中断,注入转换通道转换结束中断,模拟看门狗中断。除了产生中断外,还可以产生DMA 请求,把转换好的数据直接存储在内存里面。要注意的是只有ADC1 和ADC3 可以产生DMA 请求。
正如前面所说HAL_ADC_Start_IT()也同样能起到开启ADC的作用。
- HAL_ADC_Start_IT(&hadc1);// 中断模式
同样hal库还提供了另外的方式开启ADC。
- __HAL_ADC_ENABLE_IT(&hadc1);//标识符开启ADC中断
中断响应优先级等配置如下:
- //中断响应优先级
- HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0);
- HAL_NVIC_EnableIRQ(ADC1_2_IRQn);
- //中断函数
- void ADC1_2_IRQHandler(void);
- //处理函数
- HAL_ADC_IRQHandler(&hadc1);
- //回调函数
- HAL_ADC_ConvCpltCallback() ; // 转换完成后回调,DMA模式下DMA传输完成后调用
- HAL_ADC_ConvHalfCpltCallback() ; //转换过程中回调
- HAL_ADC_LevelOutOfWindowCallback();
- HAL_ADC_ErrorCallback();
7)获取ADC的值
配置好通道并且使能ADC后,读取ADC值。轮询方式读取,等待上一次转换结束。HAL库提供了专用强制转换函数HAL_ADC_PollForConversion,此外还提供了获取ADC的值的HAL_ADC_GetValue()。
- //强制转换函数原型
- HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef* hadc, uint32_t Timeout);
- //
- HAL_ADC_PollForConversion(&hadc1,10); //调用强制转换
- //获取ADC的值函数原型
- uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef* hadc)
- //获取ADC的值
- value = HAL_ADC_GetValue(&hadc1); //返回最近一次ADC1规则组的转换结果
8)内部温度传感器
STM32F103有一个内部的温度传感器,可以用来测量CPU及周围的温度(TA)。该温度传感器在内部和ADCx_IN16输入通道相连接,此通道把传感器输出的电压转换成数字值。HAL库里面宏定义如下:
- #define ADC_CHANNEL_TEMPSENSOR ADC_CHANNEL_16
9)ADC与DMA
- ADC_HandleTypeDef hadc3;
- DMA_HandleTypeDef hdma_adc3;
- //配置案例
- hdma_adc3.Instance = DMA2_Channel5; // 数据传输通道
- hdma_adc3.Init.Direction = DMA_PERIPH_TO_MEMORY; //存储器到外设
- hdma_adc3.Init.PeriphInc = DMA_PINC_DISABLE;
- hdma_adc3.Init.MemInc = DMA_MINC_ENABLE; //存储器增量模式
- hdma_adc3.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;//外设数据长度:16位
- hdma_adc3.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;//存储器数据长度:16位
- hdma_adc3.Init.Mode = DMA_NORMAL; //外设普通模式
- hdma_adc3.Init.Priority = DMA_PRIORITY_MEDIUM; //中等优先级
-
- if (HAL_DMA_Init(&hdma_adc3) != HAL_OK)
- {
- while(1);
- }
- //连接ADC与DMA
- __HAL_LINKDMA(&hadc3,DMA_Handle,hdma_adc3);
- //开启ADC_DMA
- HAL_ADC_Start_DMA(hadc, (uint32_t*)&ADC_ConvertedValue, 1);
DAC
STM32的DAC模块(数字/模拟转换模块)是12位数字输入,电压输出型的DAC。DAC可以配置为8位或12位模式,也可以与DMA控制器配合使用。DAC工作在12位模式时,数据可以设置成左对齐或右对齐。DAC模块有2个输出通道,每个通道都有单独的转换器。在双DAC模式下,2个通道可以独立地进行转换,也可以同时进行转换并同步地更新2个通道的输出。DAC可以通过引脚输入参考电压VREF+以获得更精确的转换结果。
STM32的DAC模块主要特点有:
①2个DAC转换器:每个转换器对应1个输出通道
②8位或者12位单调输出
③12位模式下数据左对齐或者右对齐
④同步更新功能
⑤噪声波形生成
⑥三角波形生成
⑦双DAC通道同时或者分别转换
⑧每个通道都有DMA功能
配置步骤
1)初始化DAC,设置DAC的工作模式。
DAC初始化函数HAL_DAC_Init,该函数声明如下:
- HAL_StatusTypeDef HAL_DAC_Init(DAC_HandleTypeDef* hdac);
该函数并没有对DAC进行任何配置,它只是HAL库提供用来在软件上初始化DAC,它在函数内部会调用DAC的MSP初始化函数HAL_DAC_MspInit,该函数声明如下:
- void HAL_DAC_MspInit(DAC_HandleTypeDef* hdac);
HAL库提供了一个很重要的DAC配置函数HAL_DAC_ConfigChannel,该函数用来配置DAC通道的触发类型以及输出缓冲。该函数声明如下:
- HAL_StatusTypeDef HAL_DAC_ConfigChannel(DAC_HandleTypeDef* hdac,DAC_ChannelConfTypeDef* sConfig, uint32_t Channel);
相关配置实例如下:
- //初始化
- DAC_HandleTypeDef hdac;
- hdac.Instance = DAC;
- if (HAL_DAC_Init(&hdac) != HAL_OK)
- {
- while(1);
- }
- //通道配置
- DAC_ChannelConfTypeDef sConfig = {0};
- sConfig.DAC_Trigger = DAC_TRIGGER_NONE; //不使用触发功能
- sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE;
- if (HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1) != HAL_OK)
- {
- while(1);
- }
2)使能DAC转换通道
使能DAC转换通道的函数是:
- HAL_StatusTypeDef HAL_DAC_Start(DAC_HandleTypeDef* hdac, uint32_t Channel);
- //配置实例
- HAL_DAC_Start(&hdac,DAC_CHANNEL_1); //开启DAC通道1
3)开启DAC时钟,开启PX口时钟,设置PXx为模拟输入。
- __HAL_RCC_DAC_CLK_ENABLE();
-
- __HAL_RCC_GPIOA_CLK_ENABLE();
- /**DAC GPIO Configuration*/
- GPIO_InitStruct.Pin = GPIO_PIN_4;
- GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
- HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
4)设置DAC的输出值。
HAL库函数为:
- HAL_StatusTypeDef HAL_DAC_SetValue(DAC_HandleTypeDef* hdac, uint32_t Channel, uint32_t Alignment, uint32_t Data);
- //使用
- HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,dacval);//设置DAC值
5.DAC_DMA
- /* 启动DACx DMA功能 */
- HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t *) Sine12bit, 32, DAC_ALIGN_12B_R);
DMA
DMA,全称为:Direct Memory Access,即直接存储器访问。DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,能使CPU的效率大为提高。
STM32最多有2个DMA控制器(DMA2仅存在大容量产品中),DMA1有7个通道。DMA2有5个通道。每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁起来协调各个DMA请求的优先权。
STM32的DMA有以下一些特性:
●每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置。
●在七个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),假如在相等优先权时由硬件决定(请求0优先于请求1,依此类推) 。
●独立的源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。
●支持循环的缓冲器管理
●每个通道都有3个事件标志(DMA 半传输,DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求。
●存储器和存储器间的传输
●外设和存储器,存储器和外设的传输
●闪存、SRAM、外设的SRAM、APB1 APB2和AHB外设均可作为访问的源和目标。
●可编程的数据传输数目:最大为65536
配置步骤:
1) 初始化DMA1数据流4,包括配置通道,外设地址,存储器地址,传输数据量等。
DMA的某个数据流各种配置参数初始化是通过HAL_DMA_Init函数实现的,该函数声明为:
- HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);
配置实例:
- UART_HandleTypeDef huart1;
- DMA_HandleTypeDef hdma_usart1_tx;
- /* USART1 DMA Init */
- /* USART1_TX Init */
- hdma_usart1_tx.Instance = DMA1_Channel4; //通道选择
- hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; //存储器到外设
- hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE; //外设非增量模式
- hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE; //存储器增量模式
- hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; //外设数据长度:8位
- hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; //存储器数据长度:8位
- hdma_usart1_tx.Init.Mode = DMA_NORMAL; //外设普通模式
- hdma_usart1_tx.Init.Priority = DMA_PRIORITY_MEDIUM; //中等优先级
- if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
- {
- while(1);
- }
-
2)连接DMA与外设(例如串口)
HAL库为了处理各类外设的DMA请求,在调用相关函数之前,需要调用一个宏定义标识符,来连接DMA和外设句柄。例要使用串口DMA发送,所以方式为:
- __HAL_LINKDMA(&huart,hdmatx,hdma_usart1_tx);
3)使能外设(例如:串口1)的DMA发送
- HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);//使能UART_DMA_TX
- HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart); //停止
- HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart); //暂停
- HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);//恢复
4)使能DMAx时钟。
- __HAL_RCC_DMA1_CLK_ENABLE();//DMA1时钟使能
5)使能DMAx数据流X,启动传输。(一般是一次传输使用)。
使能DMA数据流的函数为:
- HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
第二个参数是传输源地址,第三个是传输目标地址,第四个是传输的数据长度。
6)查询DMA传输状态
在DMA传输过程中,要查询DMA传输通道的状态使用的方法是:
- __HAL_DMA_GET_FLAG(&hdma_usart1_tx,DMA_FLAG_TC4);//获取DMA传输状态
获取当前传输剩余数据量:
- __HAL_DMA_GET_COUNTER(&hdma_usart1_tx);//得到当前还剩余多少个数据
7)DMA中断使用方法
DMA中断对于每个流都有一个中断服务函数,比如DMA1_Channel4的中断服务函数为DMA1_Channel4_IRQHandler。
- void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);//发送完成回调函数
- void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);/发送一半回调函数
- void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//接收完成回调函数
- void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);//接收一半回调函数
- void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);//传输出错回调函数
对于串口DMA开启,使能数据流,启动传输,这些步骤,如果使用了中断,可以直接调用HAL库函数HAL_USART_Transmit_DMA,该函数声明如下:
- HAL_StatusTypeDef HAL_USART_Transmit_DMA(USART_HandleTypeDef *husart, uint8_t *pTxData, uint16_t Size);
IIC
IIC(Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备。它是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据。在CPU与被控IC之间、IC与IC之间进行双向传送,高速IIC总线一般可达400kbps以上。I2C总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号、结束信号和应答信号。
开始信号:SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。
结束信号:SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
应答信号:接收数据的IC在接收到8bit数据后,向发送数据的IC发出特定的低电平脉冲,表示已收到数据。CPU向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。
配置过程
1)配置IIC的工作方式,进行初始化
STM32 HAL 库提供了I2C 初始化结构体及初始化函数来配置I2C 外设。
- HAL_StatusTypeDef HAL_I2C_Init(I2C_HandleTypeDef *hi2c);
配置实例如下:
- //I2C配置实例
- I2C_HandleTypeDef hi2c1;
- ///
- hi2c1.Instance = I2C1;
- hi2c1.Init.ClockSpeed = 400000;//设置SCL 时钟频率,此值要低于40 0000
- hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;//指定时钟占空比,可选low/high = 2:1 及16:9 模式
- hi2c1.Init.OwnAddress1 = 0;//指定自身的I2C 设备地址,可以是 7-bit 或者10-bit
- hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;//指定地址的长度模式
- hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;//设置双地址模式
- hi2c1.Init.OwnAddress2 = 0;//指定自身的I2C 设备地址2,只能是 7-bit
- hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;//指定广播呼叫模式
- hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;//指定禁止时钟延长模式
- if (HAL_I2C_Init(&hi2c1) != HAL_OK)
- {
- while(1);
- }
2)引脚时钟、复用配置;以及IIC时钟的开启、中断响应等级配置。
需要注意的是引脚复用应该工作在开漏模式。
- //开漏以及高速模式(而非SPI、UART的PP推挽模式)
- GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
- //I2C时钟开启
- __HAL_RCC_I2C1_CLK_ENABLE();
- //中断响应等级配置
- HAL_NVIC_SetPriority(I2C1_EV_IRQn, 0, 0);
- HAL_NVIC_EnableIRQ(I2C1_EV_IRQn);
- HAL_NVIC_SetPriority(I2C1_ER_IRQn, 0, 0);
- HAL_NVIC_EnableIRQ(I2C1_ER_IRQn);
- //
3)IIC使能、使能中断配置;
- //使能I2C
- __HAL_I2C_ENABLE(&hi2c1);
- //使能I2C中断
- __HAL_I2C_ENABLE_IT(&hi2c1,I2C_IT_BUF);
- __HAL_I2C_ENABLE_IT(&hi2c1,I2C_IT_EVT);
- //中断开启另外一种方式
- HAL_StatusTypeDef HAL_I2C_Master_Seq_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t XferOptions);
- HAL_StatusTypeDef HAL_I2C_Master_Seq_Receive_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t XferOptions);
- HAL_StatusTypeDef HAL_I2C_Slave_Seq_Transmit_IT(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size, uint32_t XferOptions);
- HAL_StatusTypeDef HAL_I2C_Slave_Seq_Receive_IT(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size, uint32_t XferOptions);
4)中断以及回调函数
- //中断
- void I2C1_ER_IRQHandler(void);
- void I2C1_EV_IRQHandler(void);
- //处理
- void HAL_I2C_EV_IRQHandler(I2C_HandleTypeDef *hi2c);
- void HAL_I2C_ER_IRQHandler(I2C_HandleTypeDef *hi2c);
- //回调函数(还有很多)
- void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c);
- void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c);
4)IIC传输函数
- //功能:在阻塞模式下将大量数据写入特定的内存地址
- HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);
- //参数://3、要写入的内存地址 uint16_t MemAddress//4、内存地址类型,是一个地址存8bit ,还是16bit数据 , uint16_t MemAddSize
- HAL_I2C_Mem_Write(&hi2c2,salve_add,0,0,PA_BUFF,sizeof(PA_BUFF),0x10);
- HAL_I2C_Mem_Read();
- //主从模式
- HAL_I2C_Master_Receive();// STM32 主机接收,不需要用到寄存器地址
- HAL_I2C_Master_Transmit(&hi2c2,salve_add,PA_BUFF,sizeof(PA_BUFF),0x10); //STM32 主机发送
- HAL_I2C_Slave_Receive();// STM32 从机机接收,不需要用到寄存器地址
- HAL_I2C_Slave_Transmit();// STM32 从机机发送,不需要用到寄存器地址
SPI
SPI 是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。SPI是Motorola研发的。SPI接口主要应用在EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。SPI是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议,STM32F1也有SPI接口。
SPI接口一般使用4条线通信:
MISO 主设备数据输入,从设备数据输出。
MOSI 主设备数据输出,从设备数据输入。
SCLK时钟信号,由主设备产生。
CS从设备片选信号,由主设备控制。
SPI主要特点有:可以同时发出和接收串行数据;可以当作主机或从机工作;提供频率可编程时钟;发送结束中断标志;写冲突保护;总线竞争保护等。
SPI总线四种工作方式SPI 模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置,时钟极性(CPOL)对传输协议没有重大的影响。如果CPOL=0,串行同步时钟的空闲状态为低电平;如果CPOL=1,串行同步时钟的空闲状态为高电平。时钟相位(CPHA)能够配置用于选择两种不同的传输协议之一进行数据传输。如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。SPI主模块和与之通信的外设备时钟相位和极性应该一致。
配置过程
1)设置SPIx工作模式。
据需求设置SPIx为主机模式或者从机模式,设置数据格式为8位,然后通过CPOL和CPHA位来设置SCK时钟极性及采样方式(查阅手册,特别重要)。设置SPI2的时钟频率(最大18Mhz),以及数据的格式(MSB在前还是LSB在前)。在HAL库中初始化SPI的函数为:
- HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi);
- //句柄
- SPI_HandleTypeDef hspi1;
- //配置实例
- hspi1.Instance = SPI1;
- hspi1.Init.Mode = SPI_MODE_MASTER; //设置SPI工作模式,设置为主模式
- hspi1.Init.Direction = SPI_DIRECTION_2LINES;//设置SPI单向或者双向的数据模式:SPI设置为双线模式
- hspi1.Init.DataSize = SPI_DATASIZE_8BIT; //设置SPI的数据大小:SPI发送接收8位帧结构
- hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; //串行同步时钟的空闲状态为高电平
- hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; //串行同步时钟的第一个跳变沿(上升或下降)数据被采样
- hspi1.Init.NSS = SPI_NSS_SOFT; //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
- hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;//定义波特率预分频的值:波特率预分频值为4
- hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
- hspi1.Init.TIMode = SPI_TIMODE_DISABLE; //关闭TI模式
- hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;//关闭硬件CRC校验
- hspi1.Init.CRCPolynomial = 10; //CRC值计算的多项式
- if (HAL_SPI_Init(&hspi1) != HAL_OK)
- {
- while(1);
- }
2)使能SPIx。
这一步通过SPI2_CR1的bit6来设置,以启动SPIx,在启动之后,可以开始SPI通讯了。库函数使能SPI1的方法为:
- __HAL_SPI_ENABLE(&SPI2_Handler); //使能SPIx
3)配置相关引脚的复用功能,使能SPIx时钟。
要用SPIx,要使能SPIx的时钟,SPIx的时钟通过APB1ENR的第14位来设置。其次要设置SPIx的相关引脚为复用输出,
使能SPIx时钟的方法为:
- __HAL_RCC_SPI2_CLK_ENABLE(); //使能SPI2时钟
4)中断配置
和串口比较类似,基本操作都一样。
响应优先级配置:
- HAL_NVIC_SetPriority(SPI1_IRQn, 0, 0);
- HAL_NVIC_EnableIRQ(SPI1_IRQn);
开启中断和中断函数的编写:
- HAL_StatusTypeDef HAL_SPI_Transmit_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);//开启发送中断(同时启用spi)
- HAL_StatusTypeDef HAL_SPI_Receive_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);//开启接受中断(同时启用spi)
- HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size);//开启接受发送中断(同时启用spi)
- //另外一种方式开启中断(只有无法开启收发中断)
- __HAL_SPI_ENABLE_IT(&hspi,SPI_IT_TXE);
- __HAL_SPI_ENABLE_IT(&hspi,SPI_IT_RXNE;
- //几个重要的回调函数
- void HAL_SPI_IRQHandler(SPI_HandleTypeDef *hspi);
- void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi);
- void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi);
- void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi);
5)SPI传输数据
通信接口当然需要有发送数据和接受数据的函数,HAL库提供的发送数据函数原型为:
往SPIx数据寄存器写入数据Data,从而实现发送。
- HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
从SPIx数据寄存器读出接受到的数据。
- HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
SPI是全双工,发送一个字节的同时接受一个字节,发送和接收同时完成,所以HAL也提供了一个发送接收统一函数:
- HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t*pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);
CAN
CAN 是Controller Area Network 的缩写(以下称为CAN),是ISO国际标准化的串行通信协议。在当前的汽车产业中,出于对安全性、舒适性、方便性、低公害、低成本的要求,各种各样的电子控制系统被开发了出来。由于这些系统之间通信所用的数据类型及对可靠性的要求不尽相同,由多条总线构成的情况很多,线束的数量也随之增加。为适应“减少线束的数量”、“通过多个LAN,进行大量数据的高速通信”的需要,1986 年德国电气商博世公司开发出面向汽车的CAN 通信协议。此后,CAN 通过ISO11898 及ISO11519 进行了标准化,现在在欧洲已是汽车网络的标准协议。
CAN 控制器根据两根线上的电位差来判断总线电平。总线电平分为显性电平和隐性电平,二者必居其一。发送方通过使总线电平发生变化,将消息发送给接收方。
CAN协议具有一下特点:
1) 多主控制。在总线空闲时,所有单元都可以发送消息(多主控制),而两个以上的单元同时开始发送消息时,根据标识符(Identifier 以下称为ID)决定优先级。ID 并不是表示发送的目的地址,而是表示访问总线的消息的优先级。两个以上的单元同时开始发送消息时,对各消息ID 的每个位进行逐个仲裁比较。仲裁获胜(被判定为优先级最高)的单元可继续发送消息,仲裁失利的单元则立刻停止发送而进行接收工作。
2) 系统的柔软性。与总线相连的单元没有类似于“地址”的信息。因此在总线上增加单元时,连接在总线上的其它单元的软硬件及应用层都不需要改变。
3) 通信速度较快,通信距离远。最高1Mbps(距离小于40M),最远可达10KM(速率低于5Kbps)。
4) 具有错误检测、错误通知和错误恢复功能。所有单元都可以检测错误(错误检测功能),检测出错误的单元会立即同时通知其他所有单元(错误通知功能),正在发送消息的单元一旦检测出错误,会强制结束当前的发送。强制结束发送的单元会不断反复地重新发送此消息直到成功发送为止(错误恢复功能)。
5) 故障封闭功能。CAN 可以判断出错误的类型是总线上暂时的数据错误(如外部噪声等)还是持续的数据错误(如单元内部故障、驱动器故障、断线等)。由此功能,当总线上发生持续数据错误时,可将引起此故障的单元从总线上隔离出去。
6) 连接节点多。CAN 总线是可同时连接多个单元的总线。可连接的单元总数理论上是没有限制的。但实际上可连接的单元数受总线上的时间延迟及电气负载的限制。降低通信速度,可连接的单元数增加;提高通信速度,则可连接的单元数减少。
配置过程:
1)设置CAN工作模式及波特率等。
在库函数中,提供了函数HAL_CAN_Init用来初始化CAN的工作模式以及波特率,HAL_CAN_Init函数体中,在初始化之前,会设置CAN_MCR寄存器的INRQ为1让其进入初始化模式,然后初始化CAN_MCR寄存器和CRN_BTR寄存器之后,会设置CAN_MCR寄存器的INRQ为0让其退出初始化模式。HAL_CAN_Init函数的声明:
- HAL_StatusTypeDef HAL_CAN_Init(CAN_HandleTypeDef *hcan);
配置案例:
- CAN_HandleTypeDef hcan;
- hcan.Instance = CAN1;
- hcan.Init.Prescaler = 16; //波特率分频器(1MBps已为stm32的CAN最高速率) (CAN 时钟频率为 APB 1 = 36 MHz)
- hcan.Init.Mode = CAN_MODE_LOOPBACK; //回环模式
- hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; //重新同步跳跃宽度 1个时间单元
- hcan.Init.TimeSeg1 = CAN_BS1_1TQ; //时间段1 占用了5个时间单元
- hcan.Init.TimeSeg2 = CAN_BS2_1TQ; //时间段2 占用了3个时间单元
- hcan.Init.TimeTriggeredMode = DISABLE; //关闭时间触发通信模式使能
- hcan.Init.AutoBusOff = ENABLE; //自动离线管理
- hcan.Init.AutoWakeUp = ENABLE; // 使用自动唤醒模式
- hcan.Init.AutoRetransmission = DISABLE;//禁止报文自动重传DISABLE-自动重传
- hcan.Init.ReceiveFifoLocked =DISABLE; 报文不锁定,新的覆盖旧的
- hcan.Init.TransmitFifoPriority = DISABLE;//发送FIFO优先级 DISABLE-优先级取决于报文标示符
- if (HAL_CAN_Init(&hcan) != HAL_OK)
- {
- while(1);
- }
2)设置滤波器。
HAL_CAN_ConfigFilter函数体中,在初始化滤波器之前,会设置CAN_FMR寄存器的FINIT位为1让其进入初始化模式,然后初始化CAN滤波器相关的寄存器之后,会设置CAN_FMR寄存器的FINIT位为0让其退出初始化模式。
- HAL_StatusTypeDef HAL_CAN_ConfigFilter(CAN_HandleTypeDef *hcan, CAN_FilterTypeDef *sFilterConfig)
配置实例:
- CAN_FilterTypeDef sFilterConfig;
-
- /*配置CAN过滤器*/
- sFilterConfig.FilterBank = 0; //过滤器0 这里可设0-13
- sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; //采用掩码模式
- sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;//32位ID
- sFilterConfig.FilterIdHigh = 0x0000; //设置过滤器ID高16位
- sFilterConfig.FilterIdLow = 0x0000; //设置过滤器ID低16位
- sFilterConfig.FilterMaskIdHigh = 0x0000; //设置过滤器掩码高16位
- sFilterConfig.FilterMaskIdLow = 0x0000; //设置过滤器掩码低16位
- sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;//过滤器0关联到FIFO0
- sFilterConfig.FilterActivation = ENABLE; //激活滤波器0
- sFilterConfig.SlaveStartFilterBank = 14;
-
- //过滤器配置
- if (HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK)
- {
- while(1){};
- }
3)CAN时钟使能以及相关的引脚配置、中断等级配置;
- //时钟开启
- __HAL_RCC_CAN1_CLK_ENABLE();
- //接收引脚
- GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
- GPIO_InitStruct.Pull = GPIO_NOPULL;
-
- //发送引脚
- GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
- //中断配置
- HAL_NVIC_SetPriority(CAN1_RX1_IRQn, 0, 0);
- HAL_NVIC_EnableIRQ(CAN1_RX1_IRQn);
- HAL_NVIC_SetPriority(CAN1_SCE_IRQn, 0, 0);
- HAL_NVIC_EnableIRQ(CAN1_SCE_IRQn);
4)使能CAN模块、使能CAN中断,中断回调编写。
- //开启CAN
- HAL_StatusTypeDef HAL_CAN_Start(CAN_HandleTypeDef *hcan);
- HAL_CAN_Start(&hcan);
- //开启接受邮箱0挂起中断
- (HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
- //标识符开启中断
- __HAL_CAN_ENABLE_IT(&hcan,CAN_IT_RX_FIFO0_MSG_PENDING);
中断以及回调:
- //中断入口
- void CAN1_RX1_IRQHandler(void);
- void CAN1_SCE_IRQHandler(void);
- //中断处理
- void HAL_CAN_IRQHandler(CAN_HandleTypeDef *hcan);
- //回调
- void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan);
- void HAL_CAN_TxMailbox0AbortCallback(CAN_HandleTypeDef *hcan);
- void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan);
- void HAL_CAN_RxFifo0FullCallback(CAN_HandleTypeDef *hcan);
5)发送接受消息
- //发送 //向 Tx 邮箱中增加一个消息,并且激活对应的传输请求
- HAL_StatusTypeDef HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan, CAN_TxHeaderTypeDef *pHeader, uint8_t aData[], uint32_t *pTxMailbox) ;
pHeader为发送的指针,aData是待发送数据,pTxMailbox为发送邮箱指针。
- //接受从Rx FIFO 收取一个 CAN 帧
- HAL_StatusTypeDef HAL_CAN_GetRxMessage(CAN_HandleTypeDef *hcan, uint32_t RxFifo, CAN_RxHeaderTypeDef *pHeader, uint8_t aData[]);
第二个为FIFO号,然后是接收指针及数据存放的地址。只需要读取pHeader便可获取接收数据和相关信息。
6)其他API
- //其他API
- HAL_CAN_RequestSleep //尝试进入休眠模式
- HAL_CAN_WakeUp //从休眠模式中唤醒
- HAL_CAN_IsSleepActive //检查是否成功进入休眠模式
- HAL_CAN_AbortTxRequest //请求中断传输
- HAL_CAN_IsTxMessagePending //检查是否有传输请求在指定的 Tx 邮箱上等待