GPIO控制

GPIO口寄存器的封装和总线时钟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//GPIO_TypeDef 结构体如下:
typedef struct
{
__IO uint32_t MODER; /*!<模式寄存器, Address offset: 0x00 */
__IO uint32_t OTYPER; /*!<输出数据类型寄存器, Address offset: 0x04 */
__IO uint32_t OSPEEDR; /*!<输出速度寄存器, Address offset: 0x08 */
__IO uint32_t PUPDR; /*!<上拉下拉电阻选择寄存器, Address offset: 0x0C */
__IO uint32_t IDR; /*!<输入数据寄存器, Address offset: 0x10 */
__IO uint32_t ODR; /*!<输出数据寄存器, Address offset: 0x14 */
__IO uint32_t BSRR; /*!< GPIO port bit set/reset register, Address offset: 0x18 */
__IO uint32_t LCKR; /*!<设置锁寄存器, Address offset: 0x1C */
__IO uint32_t AFR[2]; /*!<复用功能寄存器, Address offset: 0x20-0x24 */
} GPIO_TypeDef;
//GPIO_InitTypeDef 结构体如下:
typedef struct
{
uint32_t Pin; //引脚
uint32_t Mode; //引脚模式(推挽 or 开漏 等)
uint32_t Pull; //上拉电阻使能情况
uint32_t Speed; //引脚响应速度
uint32_t Alternate; //引脚复用情况
}GPIO_InitTypeDef;

//GPIO所在总线(AHB1)时钟使能
__HAL_RCC_GPIOx_CLK_ENABLE(); /*HAL库使用宏来进行使能, GPIOx可以是任意一组GPIO,但是对于f4系列芯片而言,他们挂载的总线一致,所以都是对AHB1总线时钟进行使能*/
/*宏内配置如下*/
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
UNUSED(tmpreg); \
} while(0U)

GPIO的初始化

1
void HAL_GPIO_Init(GPIO_TypeDef  *GPIOx, GPIO_InitTypeDef *GPIO_Init)

其中GPIOx可以直接传入GPIOA, GPIOB等等,这些已经被预编译成了以引脚初始寄存器位置开始的GPIO_TypeDef 类型的结构体,上面一节有介绍。GPIO_InitTypeDef用于控制引脚的各个功能和状态配置。

GPIO_InitTypeDef成员的值定义如下:

Pin:

Pin控制要对那一个引脚操控, 可传GPIO_PIN_x,x可以为0-15,ALL,MASK三种。

  • GPIO_PIN_0 如果将x设置为0-15,则传入的是GPIO的寄存器位置(例如,GPIO3对应的寄存器地址0B0000,0100)。多个引脚时,可以用或运算同时使能,如GPIO_PIN_0 | GPIO_PIN_1。

  • GPIO_PIN_ALL 对应的是0xFFFF,也就是将16个PIN全部操作

  • GPIO_PIN_MASK是给断言机制判断传入参数是否合法使用的,不作为用户调用参数传入。断言机制代码如下:

1
2
#define IS_GPIO_PIN(__PIN__)        ((((__PIN__) & GPIO_PIN_MASK) != (uint32_t)0x00) &&\
(((__PIN__) & ~GPIO_PIN_MASK) == (uint32_t)0x00))

这样IS_GPIO_PIN 宏定义就会在输入非GPIO合法值时返回0,触发断言机制,程序报错并定位。

Mode:

Mode控制GPIO的输入、输出模式,有如下选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define  GPIO_MODE_INPUT                //浮动输入:上拉
#define GPIO_MODE_OUTPUT_PP //推挽输出
#define GPIO_MODE_OUTPUT_OD //开漏输出
//复用PP/OD输出模式,与PP/OD出模式很是相似。只是输出的高低电平的来源不同,由ODR寄存器控制,而是用片内外设模块的复用功能输出来决定的。
//复用开漏输出功能有:(TX1,MOSI,MISO.SCK.SS)
//复用推挽输出功能有:(I2C的SCL,SDA)
#define GPIO_MODE_AF_PP //复用推挽输出
#define GPIO_MODE_AF_OD //复用开漏输出
#define GPIO_MODE_ANALOG //此时GPIO处于analog模式。可作ADC转换通道,可做比较器、DAC等模拟外设的复用通道
#define GPIO_MODE_IT_RISING //具有上升沿触发检测的外部中断模式
#define GPIO_MODE_IT_FALLING //具有下降沿触发检测的外部中断模式
#define GPIO_MODE_IT_RISING_FALLING //上升沿和下降沿均触发的外部中断模式
//下面几个对应的是"事件",RISING,FALLING等含义同中断.在这里事件可以唤醒休眠状态(WFE)的MCU,但是不会产生"中断事件",也就是说不会执行中断处理函数,也就说不需要清除外设中断挂起位或 NVIC. 使用GPIO"事件"来启动AD转换也是可以的
#define GPIO_MODE_EVT_RISING
#define GPIO_MODE_EVT_FALLING
#define GPIO_MODE_EVT_RISING_FALLING

Pull:

PULL控制上拉、下拉电阻使能情况:

1
2
3
#define  GPIO_NOPULL        ((uint32_t)0x00000000)   //上拉下拉都不使能
#define GPIO_PULLUP ((uint32_t)0x00000001) //使能上拉电阻
#define GPIO_PULLDOWN ((uint32_t)0x00000002) //使能下拉电阻

Speed

控制GPIO时钟的速度,对应也就是GPIO的响应速度.ST公司给了四种不同的相对速度(相对MCU极限速度而言)可供选择。对于不同的MCU,这里define的值是不一样的,对应的频率也是不一样的,需要参考芯片手册。对于F4系列,其定义如下

1
2
3
4
#define  GPIO_SPEED_FREQ_LOW        //2 MHz
#define GPIO_SPEED_FREQ_MEDIUM //12.5 MHz to 50 MHz
#define GPIO_SPEED_FREQ_HIGH //25 MHz to 100 MHz
#define GPIO_SPEED_FREQ_VERY_HIGH //50 MHz to 200 MHz

Alternate

控制引脚的复用情况,不同的MCU引脚可复用功能不同,需要参看stm32f4xx_hal_gpio_ex.h 头文件内的定义。例如F407系列,AF8引脚就可以配置成GPIO_AF8_UART4、GPIO_AF8_UART5、GPIO_AF8_USART6三种。详情需要参考不同芯片的手册。


GPIO的去初始化

1
void HAL_GPIO_DeInit(GPIO_TypeDef  *GPIOx, uint32_t GPIO_Pin)

这个函数用于将已初始化的GPIO恢复至默认值

  • GPIOx 同init。
  • GPIO_Pin传入要初始化第几个即可,多个Pin使用或运算合并,同Init。

读取引脚电平

1
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

用于读取GPIO的电平,读取的是IDR寄存器内对应的值。其返回值类型为GPIO_PinState,定义如下:

1
2
3
4
5
typedef enum
{
GPIO_PIN_RESET = 0,
GPIO_PIN_SET
}GPIO_PinState;

也就是RESET=0,SET=1。


写引脚电平

1
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)

用于写入GPIO某一引脚的电平。GPIOx, GPIO_Pin同上,PinState有 GPIO_PIN_RESET 和 GPIO_PIN_SET 两种


反转引脚电平

1
void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

这个函数的作用是讲某个引脚输出的值反转

这个函数的实现原理是使用BSRR寄存器,如果直接操作ODR寄存器,有被中断等(IRQ)打断的风险,而通过BSRR和BRR去改变管脚状态是不会被中断打断的,即不需要关闭中断。

BSRR的低 16bits 是set操作,而高16bits是 reset 操作。这个函数的实现原理如下

1
2
GPIOx->BSRR = ((odr & GPIO_Pin) << GPIO_NUMBER) | (~odr & GPIO_Pin);
//GPIO_NUMBER 在F4中被定义为16,因为一组GPIO有16Pin

这个语句首先将要操作PIN的ODR值放进Reset里面,然后再将这个值取反放进Set里面,如果此时这个Pin是1,那么对应寄存器的Reset就会是1,Set是0,反之亦然。这样就达到了对某一个PIN反相的效果。


锁定端口电平

1
HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

这个函数通过对LCKR寄存器进行配置,对某一个引脚输出的值进行锁定,一般用得比较少。其返回值是锁定是否成功,成功返回HAL_OK (0),失败返回HAL_ERROR (1)


GPIO触发的中断处理函数

1
2
3
4
5
6
7
8
9
10
11
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
/* EXTI line interrupt detected */
if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != RESET)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{UNUSED(GPIO_Pin);}

这个函数在中断入口的位置被自动调用。函数首先会清除中断Flag,然后调用HAL_GPIO_EXTI_Callback函数,这个回调函数就是中断要干什么。在HAL库内,有一个HAL_GPIO_EXTI_Callback已经被定义,但是使用__weak修饰,且函数只是返回一个void值,这里的意思是当用户自己定义这个中断处理函数时HAL库内这个会被disable,这个函数存在在这个只是为了防止gcc报错,且用户不可直接修改HAL库内这个callback,需要自己重新定义一个新的。

也就是说,我们需要自定义一个void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) 的函数,来写入中断操作的内容。


NVIC、EXTI和中断

代码在stm32f4xx_hal_cortex.c中

设置中断分组(NVIC)

1
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup)

其中参数如下:

  • NVIC_PRIORITYGROUP_0
  • NVIC_PRIORITYGROUP_1
  • NVIC_PRIORITYGROUP_2
  • NVIC_PRIORITYGROUP_3
  • NVIC_PRIORITYGROUP_4

image-20240202150050747

设置中断优先级(NVIC)

1
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority)

其中IRQn表示要设置的中断,PreemptPriority表示抢占优先级,SubPriority表示响应优先级

IRQn(中断号)

中断号用于指明控制哪一个中断,代码定义和对应中断如下:

它存储的是偏移地址,即相对于外部中断第一个中断寄存器WWDG_IRQn(0x0000,0040)地址的偏移量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
typedef enum
{
/****** Cortex-M4 Processor Exceptions Numbers ****************************************************************/
NonMaskableInt_IRQn = -14, /*!< 2 Non Maskable Interrupt */
MemoryManagement_IRQn = -12, /*!< 4 Cortex-M4 Memory Management Interrupt */
BusFault_IRQn = -11, /*!< 5 Cortex-M4 Bus Fault Interrupt */
UsageFault_IRQn = -10, /*!< 6 Cortex-M4 Usage Fault Interrupt */
SVCall_IRQn = -5, /*!< 11 Cortex-M4 SV Call Interrupt */
DebugMonitor_IRQn = -4, /*!< 12 Cortex-M4 Debug Monitor Interrupt */
PendSV_IRQn = -2, /*!< 14 Cortex-M4 Pend SV Interrupt */
SysTick_IRQn = -1, /*!< 15 Cortex-M4 System Tick Interrupt */
/****** STM32 specific Interrupt Numbers **********************************************************************/
WWDG_IRQn = 0, /*!< Window WatchDog Interrupt */
PVD_IRQn = 1, /*!< PVD through EXTI Line detection Interrupt */
TAMP_STAMP_IRQn = 2, /*!< Tamper and TimeStamp interrupts through the EXTI line */
RTC_WKUP_IRQn = 3, /*!< RTC Wakeup interrupt through the EXTI line */
FLASH_IRQn = 4, /*!< FLASH global Interrupt */
RCC_IRQn = 5, /*!< RCC global Interrupt */
EXTI0_IRQn = 6, /*!< EXTI Line0 Interrupt */
EXTI1_IRQn = 7, /*!< EXTI Line1 Interrupt */
EXTI2_IRQn = 8, /*!< EXTI Line2 Interrupt */
EXTI3_IRQn = 9, /*!< EXTI Line3 Interrupt */
EXTI4_IRQn = 10, /*!< EXTI Line4 Interrupt */
DMA1_Stream0_IRQn = 11, /*!< DMA1 Stream 0 global Interrupt */
DMA1_Stream1_IRQn = 12, /*!< DMA1 Stream 1 global Interrupt */
DMA1_Stream2_IRQn = 13, /*!< DMA1 Stream 2 global Interrupt */
DMA1_Stream3_IRQn = 14, /*!< DMA1 Stream 3 global Interrupt */
DMA1_Stream4_IRQn = 15, /*!< DMA1 Stream 4 global Interrupt */
DMA1_Stream5_IRQn = 16, /*!< DMA1 Stream 5 global Interrupt */
DMA1_Stream6_IRQn = 17, /*!< DMA1 Stream 6 global Interrupt */
ADC_IRQn = 18, /*!< ADC1, ADC2 and ADC3 global Interrupts */
CAN1_TX_IRQn = 19, /*!< CAN1 TX Interrupt */
CAN1_RX0_IRQn = 20, /*!< CAN1 RX0 Interrupt */
CAN1_RX1_IRQn = 21, /*!< CAN1 RX1 Interrupt */
CAN1_SCE_IRQn = 22, /*!< CAN1 SCE Interrupt */
EXTI9_5_IRQn = 23, /*!< External Line[9:5] Interrupts */
TIM1_BRK_TIM9_IRQn = 24, /*!< TIM1 Break interrupt and TIM9 global interrupt */
TIM1_UP_TIM10_IRQn = 25, /*!< TIM1 Update Interrupt and TIM10 global interrupt */
TIM1_TRG_COM_TIM11_IRQn = 26, /*!< TIM1 Trigger and Commutation Interrupt and TIM11 global interrupt */
TIM1_CC_IRQn = 27, /*!< TIM1 Capture Compare Interrupt */
TIM2_IRQn = 28, /*!< TIM2 global Interrupt */
TIM3_IRQn = 29, /*!< TIM3 global Interrupt */
TIM4_IRQn = 30, /*!< TIM4 global Interrupt */
I2C1_EV_IRQn = 31, /*!< I2C1 Event Interrupt */
I2C1_ER_IRQn = 32, /*!< I2C1 Error Interrupt */
I2C2_EV_IRQn = 33, /*!< I2C2 Event Interrupt */
I2C2_ER_IRQn = 34, /*!< I2C2 Error Interrupt */
SPI1_IRQn = 35, /*!< SPI1 global Interrupt */
SPI2_IRQn = 36, /*!< SPI2 global Interrupt */
USART1_IRQn = 37, /*!< USART1 global Interrupt */
USART2_IRQn = 38, /*!< USART2 global Interrupt */
USART3_IRQn = 39, /*!< USART3 global Interrupt */
EXTI15_10_IRQn = 40, /*!< External Line[15:10] Interrupts */
RTC_Alarm_IRQn = 41, /*!< RTC Alarm (A and B) through EXTI Line Interrupt */
OTG_FS_WKUP_IRQn = 42, /*!< USB OTG FS Wakeup through EXTI line interrupt */
TIM8_BRK_TIM12_IRQn = 43, /*!< TIM8 Break Interrupt and TIM12 global interrupt */
TIM8_UP_TIM13_IRQn = 44, /*!< TIM8 Update Interrupt and TIM13 global interrupt */
TIM8_TRG_COM_TIM14_IRQn = 45, /*!< TIM8 Trigger and Commutation Interrupt and TIM14 global interrupt */
TIM8_CC_IRQn = 46, /*!< TIM8 Capture Compare global interrupt */
DMA1_Stream7_IRQn = 47, /*!< DMA1 Stream7 Interrupt */
FSMC_IRQn = 48, /*!< FSMC global Interrupt */
SDIO_IRQn = 49, /*!< SDIO global Interrupt */
TIM5_IRQn = 50, /*!< TIM5 global Interrupt */
SPI3_IRQn = 51, /*!< SPI3 global Interrupt */
UART4_IRQn = 52, /*!< UART4 global Interrupt */
UART5_IRQn = 53, /*!< UART5 global Interrupt */
TIM6_DAC_IRQn = 54, /*!< TIM6 global and DAC1&2 underrun error interrupts */
TIM7_IRQn = 55, /*!< TIM7 global interrupt */
DMA2_Stream0_IRQn = 56, /*!< DMA2 Stream 0 global Interrupt */
DMA2_Stream1_IRQn = 57, /*!< DMA2 Stream 1 global Interrupt */
DMA2_Stream2_IRQn = 58, /*!< DMA2 Stream 2 global Interrupt */
DMA2_Stream3_IRQn = 59, /*!< DMA2 Stream 3 global Interrupt */
DMA2_Stream4_IRQn = 60, /*!< DMA2 Stream 4 global Interrupt */
ETH_IRQn = 61, /*!< Ethernet global Interrupt */
ETH_WKUP_IRQn = 62, /*!< Ethernet Wakeup through EXTI line Interrupt */
CAN2_TX_IRQn = 63, /*!< CAN2 TX Interrupt */
CAN2_RX0_IRQn = 64, /*!< CAN2 RX0 Interrupt */
CAN2_RX1_IRQn = 65, /*!< CAN2 RX1 Interrupt */
CAN2_SCE_IRQn = 66, /*!< CAN2 SCE Interrupt */
OTG_FS_IRQn = 67, /*!< USB OTG FS global Interrupt */
DMA2_Stream5_IRQn = 68, /*!< DMA2 Stream 5 global interrupt */
DMA2_Stream6_IRQn = 69, /*!< DMA2 Stream 6 global int terrupt */
USART6_IRQn = 71, /*!< USART6 global interrupt */
I2C3_EV_IRQn = 72, /*!< I2C3 event interrupt */
I2C3_ER_IRQn = 73, /*!< I2C3 error interrupt */
OTG_HS_EP1_OUT_IRQn = 74, /*!< USB OTG HS End Point 1 Out global interrupt */
OTG_HS_EP1_IN_IRQn = 75, /*!< USB OTG HS End Point 1 In global interrupt */
OTG_HS_WKUP_IRQn = 76, /*!< USB OTG HS Wakeup through EXTI interrupt */
OTG_HS_IRQn = 77, /*!< USB OTG HS global interrupt */
DCMI_IRQn = 78, /*!< DCMI global interrupt */
RNG_IRQn = 80, /*!< RNG global Interrupt */
FPU_IRQn = 81 /*!< FPU global interrupt */
} IRQn_Type;

PreemptPriority(抢占优先级)和SubPriority (响应优先级)

可输入0-15(十进制),对应抢占优先级/响应优先级的大小,数字越小优先级越高。朝IPR寄存器内写入,优先级的输入范围随Group设置范围变化。

使能/失能中断(NVIC)

1
2
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn) //使能
void HAL_NVIC_DisableIRQ(IRQn_Type IRQn) //失能

其输入参数IRQn(中断号) 同上。用于指明控制哪一个中断。

配置EXTI模式和映射IO

在HAL库中,配置EXTI寄存器上沿/下沿触发,映射IO,都通过在引脚初始化时,设置GPIO_InitTypeDef.mode实现。可设置参数如下,详见GPIO章节。

1
2
3
4
5
6
7
#define  GPIO_MODE_IT_RISING             //具有上升沿触发检测的外部中断模式
#define GPIO_MODE_IT_FALLING //具有下降沿触发检测的外部中断模式
#define GPIO_MODE_IT_RISING_FALLING //上升沿和下降沿均触发的外部中断模式
//下面几个对应的是"事件",RISING,FALLING等含义同中断.在这里事件可以唤醒休眠状态(WFE)的MCU,但是不会产生"中断事件",也就是说不会执行中断处理函数,也就说不需要清除外设中断挂起位或 NVIC. 使用GPIO"事件"来启动AD转换也是可以的
#define GPIO_MODE_EVT_RISING
#define GPIO_MODE_EVT_FALLING
#define GPIO_MODE_EVT_RISING_FALLING

清除中断flag

HAL库中中断flag会在调用回调函数前自动通过宏清除,宏定义如下:

1
#define __HAL_GPIO_EXTI_CLEAR_IT(__EXTI_LINE__) (EXTI->PR = (__EXTI_LINE__))

这个过程无需人为干预

中断Handler

在进入中断时,我们需要编写中断执行的函数,STM32不同于大部分单片机中断函数都叫interrupt,它不同的中断会调用各自不同的函数。这些中断函数在startup文件中被定义,称之为“中断向量表”。以下是定义代码和对应的名称。在使用CubeMX初始化代码时,这些Handler会被定义在stm32fxxx_it.c文件中,可以直接在it.c文件中对中断进行编辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
__Vectors       DCD     __initial_sp               ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler

; External Interrupts
DCD WWDG_IRQHandler ; Window WatchDog
DCD PVD_IRQHandler ; PVD through EXTI Line detection
DCD TAMP_STAMP_IRQHandler ; Tamper and TimeStamps through the EXTI line
DCD RTC_WKUP_IRQHandler ; RTC Wakeup through the EXTI line
DCD FLASH_IRQHandler ; FLASH
DCD RCC_IRQHandler ; RCC
DCD EXTI0_IRQHandler ; EXTI Line0
DCD EXTI1_IRQHandler ; EXTI Line1
DCD EXTI2_IRQHandler ; EXTI Line2
DCD EXTI3_IRQHandler ; EXTI Line3
DCD EXTI4_IRQHandler ; EXTI Line4
DCD DMA1_Stream0_IRQHandler ; DMA1 Stream 0
DCD DMA1_Stream1_IRQHandler ; DMA1 Stream 1
DCD DMA1_Stream2_IRQHandler ; DMA1 Stream 2
DCD DMA1_Stream3_IRQHandler ; DMA1 Stream 3
DCD DMA1_Stream4_IRQHandler ; DMA1 Stream 4
DCD DMA1_Stream5_IRQHandler ; DMA1 Stream 5
DCD DMA1_Stream6_IRQHandler ; DMA1 Stream 6
DCD ADC_IRQHandler ; ADC1, ADC2 and ADC3s
DCD CAN1_TX_IRQHandler ; CAN1 TX
DCD CAN1_RX0_IRQHandler ; CAN1 RX0
DCD CAN1_RX1_IRQHandler ; CAN1 RX1
DCD CAN1_SCE_IRQHandler ; CAN1 SCE
DCD EXTI9_5_IRQHandler ; External Line[9:5]s
DCD TIM1_BRK_TIM9_IRQHandler ; TIM1 Break and TIM9
DCD TIM1_UP_TIM10_IRQHandler ; TIM1 Update and TIM10
DCD TIM1_TRG_COM_TIM11_IRQHandler ; TIM1 Trigger and Commutation and TIM11
DCD TIM1_CC_IRQHandler ; TIM1 Capture Compare
DCD TIM2_IRQHandler ; TIM2
DCD TIM3_IRQHandler ; TIM3
DCD TIM4_IRQHandler ; TIM4
DCD I2C1_EV_IRQHandler ; I2C1 Event
DCD I2C1_ER_IRQHandler ; I2C1 Error
DCD I2C2_EV_IRQHandler ; I2C2 Event
DCD I2C2_ER_IRQHandler ; I2C2 Error
DCD SPI1_IRQHandler ; SPI1
DCD SPI2_IRQHandler ; SPI2
DCD USART1_IRQHandler ; USART1
DCD USART2_IRQHandler ; USART2
DCD USART3_IRQHandler ; USART3
DCD EXTI15_10_IRQHandler ; External Line[15:10]s
DCD RTC_Alarm_IRQHandler ; RTC Alarm (A and B) through EXTI Line
DCD OTG_FS_WKUP_IRQHandler ; USB OTG FS Wakeup through EXTI line
DCD TIM8_BRK_TIM12_IRQHandler ; TIM8 Break and TIM12
DCD TIM8_UP_TIM13_IRQHandler ; TIM8 Update and TIM13
DCD TIM8_TRG_COM_TIM14_IRQHandler ; TIM8 Trigger and Commutation and TIM14
DCD TIM8_CC_IRQHandler ; TIM8 Capture Compare
DCD DMA1_Stream7_IRQHandler ; DMA1 Stream7
DCD FMC_IRQHandler ; FMC
DCD SDIO_IRQHandler ; SDIO
DCD TIM5_IRQHandler ; TIM5
DCD SPI3_IRQHandler ; SPI3
DCD UART4_IRQHandler ; UART4
DCD UART5_IRQHandler ; UART5
DCD TIM6_DAC_IRQHandler ; TIM6 and DAC1&2 underrun errors
DCD TIM7_IRQHandler ; TIM7
DCD DMA2_Stream0_IRQHandler ; DMA2 Stream 0
DCD DMA2_Stream1_IRQHandler ; DMA2 Stream 1
DCD DMA2_Stream2_IRQHandler ; DMA2 Stream 2
DCD DMA2_Stream3_IRQHandler ; DMA2 Stream 3
DCD DMA2_Stream4_IRQHandler ; DMA2 Stream 4
DCD ETH_IRQHandler ; Ethernet
DCD ETH_WKUP_IRQHandler ; Ethernet Wakeup through EXTI line
DCD CAN2_TX_IRQHandler ; CAN2 TX
DCD CAN2_RX0_IRQHandler ; CAN2 RX0
DCD CAN2_RX1_IRQHandler ; CAN2 RX1
DCD CAN2_SCE_IRQHandler ; CAN2 SCE
DCD OTG_FS_IRQHandler ; USB OTG FS
DCD DMA2_Stream5_IRQHandler ; DMA2 Stream 5
DCD DMA2_Stream6_IRQHandler ; DMA2 Stream 6
DCD DMA2_Stream7_IRQHandler ; DMA2 Stream 7
DCD USART6_IRQHandler ; USART6
DCD I2C3_EV_IRQHandler ; I2C3 event
DCD I2C3_ER_IRQHandler ; I2C3 error
DCD OTG_HS_EP1_OUT_IRQHandler ; USB OTG HS End Point 1 Out
DCD OTG_HS_EP1_IN_IRQHandler ; USB OTG HS End Point 1 In
DCD OTG_HS_WKUP_IRQHandler ; USB OTG HS Wakeup through EXTI
DCD OTG_HS_IRQHandler ; USB OTG HS
DCD DCMI_IRQHandler ; DCMI
DCD 0 ; Reserved
DCD HASH_RNG_IRQHandler ; Hash and Rng
DCD FPU_IRQHandler ; FPU


__Vectors_End

HAL库的中断处理流程

CubeMX+HAL工程中断初始化的流程

如果我们采用寄存器编程,以初始化映射至IO的EXTI中断为例,那么需要经历以下步骤:

  1. 使能GPIO时钟和NVIC、EXTI总线时钟
  2. 设置GPIO模式(输入、是否上拉等)
  3. 设置EXTI和GPIO的映射关系(设置APIO或STYSCFG寄存器)
  4. 设置边沿触发模式(RTSR,FTSR)、enbale中断(IMR/EMR)
  5. 设置NVIC分组模式
  6. 设置NVIC优先级模式
  7. 使能NVIC对应中断

在使用CubeMX生成的工程中,1,2,3,4都是在MX_GPIO_Init()函数内完成。如果是映射至IO的EXTI的中断,那么MX_GPIO_Init()函数内还会完成相应中断的优先级设置和使能,也就是6,7;如果是其他中断,那么在相应外设的init函数中也会完成优先级设置和使能。

鉴于NVIC的分组模式一般不会更改,因此NVIC的分组在一开始就被设置,CubeMX+HAL库生成的工程会在HAL_Init()函数中完成。

main函数中,会依次调用这三个函数来完成整个系统的初始化,其中便囊括了中断的初始化:

1
2
3
HAL_Init();//完成步骤5,设置分组
SystemClock_Config();
MX_GPIO_Init();//完成步骤1.2.3.4.6.7;初始化GPIO并配置和映射EXTI线,然后使能NVIC对应的中断

中断调用函数的流程

在中断被触发时,STM32首先会调用Handler,也就是下方 中断Handler 小节内介绍的这些。这些Handler的内容存储在stm32fxxx_it.c。Hander函数内首先调用公共中断函数,来实现清除中断标志等,然后调用回调函数执行处理的内容。这里面用户可直接编辑Handler函数的内容,来决定其调用哪些公共函数,也可自行重新定义Callback函数的内容,来编写中断处理程序。

这个流程的函数伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void xxx_IRQHandler(){
//public interrupt functions
xxx_public_INT_Func(specific parameters);//e.g. HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
}
void xxx_public_INT_Func(parameters){
public_funcs(parameters);// e.g. __HAL_GPIO_EXTI_CLEAR_IT(GOIO_Pin)
Callback_func(parameters);// e.g. HAL_GPIO_EXTI_Callback(GPIO_Pin)
}
void Callback_func(parameters){
if (parameters == xx){
//do sth
}
if (parameters == yy){
//do sth
}
//interrupt Processing code, written by user
}

一般来说,callback函数是弱定义的,例如EXTI的回调函数定义如下:

1
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)

因此,在HAL库中使用中断时,用户只需要自定重新定义回调函数部分,找到不同中断Handler调用的回调函数,在回调函数部分写入中断处理的代码即可。通常来说,用户只需要自己重新定义Callback fucntion,并根据传入的参数不同,使用if编写不同中断源的处理程序即可。但是再多个外设共用一个回调函数时,这些if可能需要写非常多,这样不便于管理,因此直接把处理函数写在其自己的驱动文件内,然后在xxx_IRQHandler() 内调用即可。


UART串口

huart句柄

这是一个调用和uart相关HAL库接口时,需要传入的参数,用于说明数据从哪个串口进来,串口的基本设置参数。这个参数被称为”handel”,也就是huart中h的来源,可以理解为握着uart1的handel,就可以控制和操作uart1的意思。其源代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
typedef struct __UART_HandleTypeDef
{
USART_TypeDef *Instance; /*!< UART 控制寄存器基地址 */
UART_InitTypeDef Init; /*!< UART 串口初始化数据类型 */
const uint8_t *pTxBuffPtr; /*!< 发送buffer地址 */
uint16_t TxXferSize; /*!< 发送数据个数 */
__IO uint16_t TxXferCount; /*!< 发送数据计数器 */
uint8_t *pRxBuffPtr; /*!< 接收buffer地址*/
uint16_t RxXferSize; /*!< 接收数据个数 */
__IO uint16_t RxXferCount; /*!< 接收数据计数器 */
__IO HAL_UART_RxTypeTypeDef ReceptionType; /*!< Type of ongoing reception */
__IO HAL_UART_RxEventTypeTypeDef RxEventType; /*!< Type of Rx Event */
DMA_HandleTypeDef *hdmatx; /*!< UART Tx DMA Handle parameters */
DMA_HandleTypeDef *hdmarx; /*!< UART Rx DMA Handle parameters */
HAL_LockTypeDef Lock; /*!< Locking object */
__IO HAL_UART_StateTypeDef gState; /*!< UART state information related to global Handle management
and also related to Tx operations.
This parameter can be a value of @ref HAL_UART_StateTypeDef */
__IO HAL_UART_StateTypeDef RxState; /*!< UART state information related to Rx operations.
This parameter can be a value of @ref HAL_UART_StateTypeDef */
__IO uint32_t ErrorCode; /*!< UART Error code */
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
void (* TxHalfCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Tx Half Complete Callback */
void (* TxCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Tx Complete Callback */
void (* RxHalfCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Rx Half Complete Callback */
void (* RxCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Rx Complete Callback */
void (* ErrorCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Error Callback */
void (* AbortCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Complete Callback */
void (* AbortTransmitCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Transmit Complete Callback */
void (* AbortReceiveCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Receive Complete Callback */
void (* WakeupCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Wakeup Callback */
void (* RxEventCallback)(struct __UART_HandleTypeDef *huart, uint16_t Pos); /*!< UART Reception Event Callback */

void (* MspInitCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Msp Init callback */
void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Msp DeInit callback */
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */

} UART_HandleTypeDef;

USART_TypeDef

该结构体用于参数UASRT硬件的寄存器基地址,结构体定义如下:

在创建 $USART_TypeDef\ *$ 类型,以某一个串口配置寄存器开始的结构体指针后,便可以访问该串口的配置寄存器。

1
2
3
4
5
6
7
8
9
10
typedef struct
{
__IO uint32_t SR; /*!< USART Status register, Address offset: 0x00 */
__IO uint32_t DR; /*!< USART Data register, Address offset: 0x04 */
__IO uint32_t BRR; /*!< USART Baud rate register, Address offset: 0x08 */
__IO uint32_t CR1; /*!< USART Control register 1, Address offset: 0x0C */
__IO uint32_t CR2; /*!< USART Control register 2, Address offset: 0x10 */
__IO uint32_t CR3; /*!< USART Control register 3, Address offset: 0x14 */
__IO uint32_t GTPR; /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef;

该结构体的基地址变量,由总线基地址+偏移地址宏定义,例如F407定义的USART1和2

1
2
3
4
#define USART1_BASE           (APB2PERIPH_BASE + 0x1000UL)
#define USART2_BASE (APB1PERIPH_BASE + 0x4400UL)
#define USART1 ((USART_TypeDef *) USART1_BASE)
#define USART2 ((USART_TypeDef *) USART2_BASE)

对于F4系列,有6个串口:USART1-3,UART4-5,USART6,他们都通过如上代码宏定义了基地址,因此给结构体成员Instance传入这些地址即可。

UART_InitTypeDef

该结构体用于存放UART通信中的一些基本参数,代码如下:

1
2
3
4
5
6
7
8
9
10
typedef struct
{
uint32_t BaudRate; //波特率
uint32_t WordLength; //数据长度
uint32_t StopBits; //停止位长度
uint32_t Parity; //奇偶校验配置
uint32_t Mode;// UART收发模式设置(收、发、同时收发)
uint32_t HwFlowCtl; //硬件流控制
uint32_t OverSampling; //过采样设置
} UART_InitTypeDef;
  • BaudRate: 直接设置波特率数字即可

  • WordLength: 宏定义了 UART_WORDLENGTH_8B 和 UART_WORDLENGTH_9B 两种,分别对应8bit和9bit数据位

  • StopBits: 宏定义 UART_STOPBITS_1,UART_STOPBITS_2 两种输入,分别对应1个停止位和2个

  • Parity:有 UART_PARITY_NONE,UART_PARITY_EVEN,UART_PARITY_ODD 三种,分别对应无、偶校验、奇校验
  • Mode: 有UART_MODE_RX,UART_MODE_TX,UART_MODE_TX_RX 三种,分别对应单收、单发、同时收发三种工作模式
  • HwFlowCtl: 有UART_HWCONTROL_NONE,UART_HWCONTROL_RTS,UART_HWCONTROL_CTS,UART_HWCONTROL_RTS_CTS四种输入,分别对应无硬件流控制,接受流控制,发送流控制,同时启用接受发送流控制。
  • OverSampling: UART_OVERSAMPLING_16,UART_OVERSAMPLING_8两种,对应16位过采样和8位,F1系列不可配置

串口初始化

轮询方式收发的初始化

使用轮询模式时,只需要先配置huart中Init结构体的参数,然后再调用HAL_UART_Init函数进行寄存器配置即可。下面是一个例子

1
2
3
4
5
6
7
8
9
10
11
12
huart1.Instance = USART3;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}

在HAL_UART_Init()函数中,会依次执行如下操作:

  • 若检测为轮询模式的初始化,则会调用HAL_UART_MspInit()函数对其所用的总线时钟、GPIO等关联设备进行初始化
  • UART_SetConfig() 函数来对控制寄存器写入
  • 对句柄结构体中所有状态指示器写入OK的状态

其中HAL_UART_MspInit()函数一个例子如下,它会对所有关联的设备进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{

GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance==USART1)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;//该GPIO复用设置为GPIO_AF7_USART1
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}

在使用CubeMX配置工程时,会生成usart.c文件来存放各项init函数,例如总初始化函数MX_USART1_UART_Init()、关联设备初始化函数HAL_UART_MspIni(),以备HAL库文件stm32fxxx_hal_uart.c调用

轮询(阻塞)数据收发

在轮询数据模式下,MCU专注于发送数据或接受数据,在Timeout规定的时间内收发完成则return 成功,否则return超时。在这个期间,MCU一直除以等数据接受或者看着输出发送的状态,效率较低。

轮询串口接收

1
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)

huart句柄已在上方介绍,剩余输入参数分别是:

  • pData:存放数据的buffer首地址
  • Size:接受数据的字符串长度
  • Timeout:轮询时长,超时则返回

这个函数会返回的类型”HAL_StatusTypeDef”有如下状态:

1
2
3
4
5
6
7
typedef enum 
{
HAL_OK = 0x00U, //成功接收
HAL_ERROR = 0x01U, //出现错误
HAL_BUSY = 0x02U, //串口忙
HAL_TIMEOUT = 0x03U //轮询超时
} HAL_StatusTypeDef;

轮询串口发送

1
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout)

其输入参数、返回参数定义均同轮询接收函数一样,这里不重复介绍。

中断数据收发

使用中断模式进行数据收发时,MCU进会在有数据进来的时候让CPU去处理数据。其余时候可以干其他事情。这样效率较高

中断串口接收

1
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

参数如下:

  • huart:uart handle地址
  • pData:存放数据的buffer首地址
  • Size:接受数据的字符串长度

这里面输入的Size参数会被赋值进handle内的RxXferSize成员内。pData会被赋值进handle内的pRxBuffPtr成员内。在USART中断函数

HAL_UART_IRQHandler()中,会首先判断有没有错误出现,如果没有错误,则调用UART_Receive_IT()函数进行接收。这两个函数都被定义在stm32xxx_hal_uart.c内。

UART_Receive_IT函数中,每接收一个数据,就会对pRxBuffPtr的指针地址进行位移,来指向下一个内存单元的地址。同时对RxXferCount自减。当RxXferCount自减至0时,会关闭USART接受相关的中断使能。这一段代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static HAL_StatusTypeDef UART_Receive_IT(UART_HandleTypeDef *huart)
{
uint8_t *pdata8bits;
uint16_t *pdata16bits;
if (huart->RxState == HAL_UART_STATE_BUSY_RX)
{
if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
{
pdata8bits = NULL;
pdata16bits = (uint16_t *) huart->pRxBuffPtr;
*pdata16bits = (uint16_t)(huart->Instance->DR & (uint16_t)0x01FF);
huart->pRxBuffPtr += 2U;
}
else
{
pdata8bits = (uint8_t *) huart->pRxBuffPtr;
pdata16bits = NULL;

if ((huart->Init.WordLength == UART_WORDLENGTH_9B) || ((huart->Init.WordLength == UART_WORDLENGTH_8B) && (huart->Init.Parity == UART_PARITY_NONE)))
{
*pdata8bits = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF);
}
else
{
*pdata8bits = (uint8_t)(huart->Instance->DR & (uint8_t)0x007F);
}
huart->pRxBuffPtr += 1U;
}
if (--huart->RxXferCount == 0U)
{
__HAL_UART_DISABLE_IT(huart, UART_IT_RXNE);
__HAL_UART_DISABLE_IT(huart, UART_IT_PE);
__HAL_UART_DISABLE_IT(huart, UART_IT_ERR);
huart->RxState = HAL_UART_STATE_READY;
huart->RxEventType = HAL_UART_RXEVENT_TC;
if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE)
{
huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;
ATOMIC_CLEAR_BIT(huart->Instance->CR1, USART_CR1_IDLEIE);
if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE))
__HAL_UART_CLEAR_IDLEFLAG(huart);
//...未完
}

因此,调用HAL_UART_Receive_IT()后便会自动使能UART接收相关串口中断,同时准备好接受。在当接受的字符>=Size所指定的字符数之后,便会自动失能相关中断

在UART_Receive_IT()中,如果指定的字符数已经接收完成(RxXferCount == 0),则会调用HAL_UART_RxCpltCallback()这一公共回调函数。因为HAL库的库文件需要把硬件抽象成接口,因此直接改库文件内的代码不利于移植,用户的自定义中断回到函数写在HAL_UART_RxCpltCallback()内即可。此时UART已经将接收到的字符存在了pData指定的地址内,在HAL_UART_RxCpltCallback()内对pData内的数据进行处理即可。如果处理完成还需继续接收下一个数据,则再次调用HAL_UART_Receive_IT()来启用中断准备接收。

举个例子,假设我使用的通信协议规定每8byte数据为以通信帧,一数据帧传送数据位1byte,那么就可以把Size设置为8,然后每8byte数据接收完成之后就会调用用户自定义的回调函数HAL_UART_RxCpltCallback()函数来让用户处理数据。

下面是一个例程,实现使用中断接受UART数据,然后在收到换行符时把接收的数据发回去(省去了系统初始化部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
uint8_t ch;
uint8_t count=0;
uint8_t buffer[13]= {0};
int main(void){
HAL_UART_Receive_IT(&huart1,&ch,1);
while(1);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
//user code here
buffer[count] = ch; //ch就是pData指向的变量地址
count++;
if(buffer[count-1] == '\n'){
HAL_UART_Transmit(huart, buffer, count , 100);//把收到的字符发回串口
memset(buffer, '\0', sizeof(buffer));
count = 0;
}
HAL_UART_Receive_IT(&huart1,&ch,1);
}

SPI 通信