STM32 开发基础知识入门
1. MDK C 语言基础复习
1.1 位操作
[tr]运算符含义运算符含义[/tr]
- 不改变其他位值的情况下,对某几个位进行设值,方法是对需要设置的位用& 操作符进行清零操作,然后用| 操作符设值
GPIOA->CRL&=0XFFFFFF0F; //将第 4-7 位清 0,转换BIN进制就清晰了 然后再与需要设置的值进行|或运算
GPIOA->CRL|=0X00000040; //设置相应位的值,不改变其他位的值
- 移位操作提高代码的可读性
GPIOx->BSRR = (((uint32_t)0x01) << pinpos); 将0x01 这个1转换成unsigned int 类型,0x01 是8位二进制0000 0001转换成32位的0x0001,因为BSRR 是32位寄存器,将1 转换成32位之后左移pinpos 这个变量值
GPIOA->ODR|=1<<5//PA.5 输出高,不改变其他位/*GPIOA->ODR |= 1<<5 拆分为:unsigned char a;a=1<<5;// a= 0010 0000GPIOA->ODR = GPIOA->ODR |a;*/
- ~取反操作使用技巧
SR 寄存器的每一位都代表一个状态,某个时刻我们希望去设置某一位的值为 0,同时
其他位都保留为 1,简单的作法是直接给寄存器设置一个值:TIMx->SR=0xFFF7;
库函数 代码中怎样使用的:
TIMx->SR = (uint16_t)~TIM_FLAG_Update;// 将TIM_FLAG 取反,然后强制类型转换成uint_16,然后将TIMx→ SR#define TIM_FLAG_Update ((uint16_t)0x0001)// BIT0#define TIM_FLAG_CC1 ((uint16_t)0x0002)//BIT1, 不需要& 是因为该register 写1无效,写0清零
1.2 typedef 类型命名
typedef 用于定义结构体的类别名和枚举类型
typedef struct{__IO uint32_t CRL;__IO uint32_t CRH;…} GPIO_TypeDef;// Typedef 为结构体定义一个别名 GPIO_TypeDef,这样我们可以通过 GPIO_TypeDef 来定义结构体变量:GPIO_TypeDef _GPIOA,_GPIOB; 1.3 结构体
//声明结构体类型:Struct 结构体名{成员列表;}变量名列表;//例如:Struct U_TYPE { Int BaudRate Int WordLength;}usart1,usart2; 结构体变量名字.成员名, 引用usart1 的成员Baud Rate,方法是usart1.Baud Rate,
struct U_TYPE *usart3;// 定义结构体指针变量 usart3 结构体指针成员变量引用方法是通过"→" 符号实现,Usart3 ->BaudRate
typedef struct{uint32_t USART_BaudRate;uint16_t USART_WordLength;uint16_t USART_StopBits;uint16_t USART_Parity;uint16_t USART_Mode;uint16_t USART_HardwareFlowControl;} USART_InitTypeDef; 1.4 define 宏定义
# define SYSCLK_FREQ_72MHz 72000000 // # define 标识符 字符串 (可以是常数、表达式、格式串) 1.5 ifdef 条件编译
当满足某条件时对一组语句进行编译,而当条件不满足时则编译另外一组语句,常见
# iddef 标识符程序段1# else程序段2#endif 标识符已经被定义过,则对程序段1 进行编译,否则编译程序段2 ,其中# else 也可以没有
1.6 extern 变量申明
C 语言中extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中提示编译器遇到此变量和函数时在其他模块中寻找定义。对于extern 申明变量可以多次,但是定义只有一次
在 Main.c 定义的全局变量 id,id 的初始化都是在 Main.c 里面进行的
u8 id;//定义只允许一次 main() { id=1; printf("d%",id);//id=1 test(); printf("d%",id);//id=2 } 但是我们希望在test.c的 changeId(void)函数中使用变量id,这个时候我们就需要在test.c里面去申明变量 id 是外部定义的了,因为如果不申明,变量 id 的作用域是到不了 test.c 文件中。看下面 test.c 中的代码:
extern u8 id;//申明变量 id 是在外部定义的,申明可以在很多个文件中进行void test(void){ id=2;} 1.7 static 关键字
函数分为内部函数和外部函数。当一个源程序由多个源文件组成时,C语言根据函数能否被其它源文件中的函数调用,将函数分为内部函数和外部函数。
如果在一个源文件中定义的函数,只能被本文件中的函数调用,而不能被同一程序其它文件中的函数调用,这种函数称为内部函数。定义一个内部函数,只需在函数类型前再加一个“static”关键字即可,如下所示
static 函数类型 函数名(函数参数表){……}
**外部函数的定义:**在定义函数时,如果没有加关键字“static”,或冠以关键字“extern”,表示此函数是外部函数
static 申明的局部变量,存储在静态存储区中,它在函数调用结束之后,不会被释放,它的值会一直保留下来,所以static 申明的局部变量具有记忆功能
2.STM32 时钟系统
STM32 具有5个时钟源:HSI, HSE, LSI, LSE,PLL ,其中HIS ,HSE 以及PLL 是高速时钟,LSI 和LSE 是低速时钟;从来源来看,分为外部时钟源和内部时钟源。当需要使用某模块时,记得一定要先使能对应的时钟。
HSI:高速内部时钟,频率8MHZ
HSE: 高度外部时钟,频率范围是4~16MHZ ,外接石英/陶瓷谐振
LSI: 低速内部时钟,40KHZ,独立看门狗时钟源是LSI ,LSI 还可以是RTC 时钟源
LSE: 低速外部时钟,主要是RTC 的时钟源
PLL:锁相环倍频输出,倍频可以选择2~16位
A. MCO 是 STM32 的一个时钟输出 IO(PA8),它可以选择一个时钟信号输出, 可以选择为 PLL 输出的 2 分频、 HSI、 HSE、或者系统时钟。这个时钟可以用来给外部其他系统提供时钟源。
B. 这里是 RTC 时钟源,从图上可以看出, RTC 的时钟源可以选择 LSI, LSE,以及HSE 的 128 分频。
C. 从图中可以看出 C 处 USB 的时钟是来自 PLL 时钟源。 STM32 中有一个全速功能的 USB 模块,其串行接口引擎需要一个频率为 48MHz 的时钟源。该时钟源只能从 PLL 输出端获取,可以选择为 1.5 分频或者 1 分频,也就是,当需要使用 USB模块时, PLL 必须使能,并且时钟频率配置为 48MHz 或 72MHz。
D. D 处就是 STM32 的系统时钟 SYSCLK,它是供 STM32 中绝大部分部件工作的时钟源。 系统时钟可选择为 PLL 输出、 HSI 或者 HSE。系统时钟最大频率为 72MHz,当然你也可以超频,不过一般情况为了系统稳定性是没有必要冒风险去超频的。
E. 这里的 E 处是指其他所有外设了。从时钟图上可以看出,其他所有外设的时钟最终来源都是 SYSCLK。 SYSCLK 通过 AHB 分频器分频后送给各模块使用。这些模块包括:
①、 AHB 总线、内核、内存和 DMA 使用的 HCLK 时钟。
②、通过 8 分频后送给 Cortex 的系统定时器时钟,也就是 systick 了。
③、直接送给 Cortex 的空闲运行时钟 FCLK。
④、送给 APB1 分频器。 APB1 分频器输出一路供 APB1 外设使用(PCLK1,最大
频率 36MHz),另一路送给定时器(Timer)2、 3、 4 倍频器使用。
⑤、送给 APB2 分频器。 APB2 分频器分频输出一路供 APB2 外设使用(PCLK2,
最大频率 72MHz),另一路送给定时器(Timer)1 倍频器使用。
SystemInit()函数中设置的系统时钟大小:
[tr][/tr]
AHB 总线时钟(使用 SYSCLK) | =72MHz | APB1 总线时钟(PCLK1) | =36MHz | APB2 总线时钟(PCLK2) | =72MHz | PLL 时钟 | =72MHz | SYSCLK(系统时钟) | =72MHz | 3. 寄存器地址名称映射分析
# define GPIOA ((GPIO_TypeDef *)GPIOA_BASE) //GPIOA 是将GPIOA_BASE 强制转换成GPIO_Typedef 指针,GPIOA 指向地址GPIOA_BASE,GPIOA_BASE 存放的数据类型位GPIO_TypedefGPIOA_BASE = 0x40000000 + 0x10000 + 0x0800 = 0x40010800 // GPIOA 的基地址是0x40018000// GPIOA 的寄存器地址= GPIOA 基地址+ 寄存器相对GPIOA 基地址的移值
[tr]寄存器偏移地址实际地址=基地址+偏移地址[/tr]
GPIOA->CRL | 0x00 | 0x40010800+0x00 | GPIOA->CRH; | 0x04 | 0x40010800+0x04 | GPIOA->IDR; | 0x08 | 0x40010800+0x08 | GPIOA->ODR | 0x0c | 0x40010800+0x0c | GPIOA->BSRR | 0x10 | 0x40010800+0x10 | GPIOA->BRR | 0x14 | 0x40010800+0x14 | GPIOA->LCKR | 0x18 | 0x40010800+0x18 | 4.STM32 系统架构
STM32 主系统主要由4个驱动单元和四个被动单元构成:
**四个驱动单元:**内核DCode 总线, 系统总线,通用DMA1, 通用DMA2
**四被动单元:**AHB 到APB 的桥:连接所有APB 设备。内部FLASH 内存,内部SRAM, FSMC
- ICode 总线:该总线将 M3 内核指令总线和闪存指令接口相连,指令的预取在该总线上
面完成。
- DCode 总线:该总线将 M3 内核的 DCode 总线与闪存存储器的数据接口相连接,常量
加载和调试访问在该总线上面完成。
- 系统总线:该总线连接 M3 内核的系统总线到总线矩阵,总线矩阵协调内核和 DMA 间
访问。
- DMA 总线:该总线将 DMA 的 AHB 主控接口与总线矩阵相连,总线矩阵协调 CPU 的
DCode 和 DMA 到 SRAM,闪存和外设的访问。
- 总线矩阵:总线矩阵协调内核系统总线和 DMA 主控总线之间的访问仲裁,仲裁利用
轮换算法。
- AHB/APB 桥:这两个桥在 AHB 和 2 个 APB 总线间提供同步连接, APB1 操作速度限于
36MHz,APB2 操作速度全速。
5.端口复用和重映射
5.1 端口复用
STM32F103ZET6 有 5 个串口,我们可以查手册知道,串口 1 的引脚对应的 IO 为 PA9,PA10,PA9, PA10 默认功能是 GPIO, 所以当PA9,PA10 引脚作为串口 1 的 TX,RX 引脚使用的时候,那就是端口复用 。
复用步骤如下:
- GPIO 端口时钟使能。要使用到端口复用,当然要使能端口的时钟了。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
- 复用的外设时钟使能。比如你要将端口 PA9,PA10 复用为串口,所以要使能串口时钟。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
- 端口模式配置。 在 IO 复用位内置外设功能引脚的时候,必须设置 GPIO 端口的模式,至于在复用功能下 GPIO 的模式是怎么对应的,这个可以查看手册《STM32 中文参考手册 V10》P110 的表格“8.1.11 外设的 GPIO 配置”。这里我们拿 Usart1 举例:
5.2 端口重映射
在 STM32 中引入了外设引脚重映射的概念,即一个外设的引脚除了具有默认的端口外,还可以通过设置重映射寄存器的方式,把这个外设的引脚映射到其它的端口。 简单的讲就是把管脚的外设功能映射到另一个管脚,但不是可以随便映射的,具体对应关系《STM32 中文参考手册 V10》的 P116 页**“8.3 复用功能和调试配置**”有讲解。
默认情况下,串口 1 复用的时候的引脚位 PA9,PA10,同时我们可以将 TX 和 RX 重新映射到管脚 PB6 和 PB7 上面去。 所以重映射我们同样要使能复用功能的时候讲解的 2 个时钟外,还要使能 AFIO 功能时钟,然后要调用重映射函数。 详细步骤为:
- 使能GPIOB 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
- 使能串口1 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
- 使能AFIO 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
- 开启重映射
GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE); 至于有哪些功能可以重映射,大家除了查看中文参考手册之外,还可以从 GPIO_PinRemapConfig 函数入手查看第一个入口参数的取值范围可以得知。
对于 USART3,存在部分重映射和完全重映射。 所谓部分重映射就是部分管脚和默认的是一样的,而部分管脚是重新映射到其他管脚。 而完全重映射就是所有管脚都重新映射到其他管脚。 看看手册中的 USART3 重映射表:
部分重映射就是 PB10,PB11,PB12 重映射到 PC10,PC11, PC12 上。而 PB13 和 PB14 和没有重映射
情况是一样的,都是 USART3_CTS 和 USART3_RTS 对应管脚。 完全重映射就是将这两个脚重新映
射到 PD11 和 PD12 上去。 我们要使用 USART3 的部分重映射,我们调用函数方法为:
GPIO_PinRemapConfig(GPIO_PartialRemap_USART3, ENABLE);
6.STM32 NVIC 中断优先级
STM32 有 84 个中断,包括 16 个内核中断和 68 个可屏蔽中断,具有 16 级可编程的中断优先级。而我们常用的就是这 68 个可屏蔽中断, 但是 STM32 的 68 个可屏蔽中断,在 STM32F103 系列上面,又只有 60 个(在 107 系列才有 68 个)。
在 MDK 内,与 NVIC 相关的寄存器, MDK 为其定义了如下的结构体:
typedef struct{__IO uint32_t ISER[8]; /*!< Interrupt Set Enable Register */uint32_t RESERVED0[24];__IO uint32_t ICER[8]; /*!< Interrupt Clear Enable Register */uint32_t RSERVED1[24];__IO uint32_t ISPR[8]; /*!< Interrupt Set Pending Register */uint32_t RESERVED2[24];__IO uint32_t ICPR[8]; /*!< Interrupt Clear Pending Register */uint32_t RESERVED3[24];__IO uint32_t IABR[8]; /*!< Interrupt Active bit Register */uint32_t RESERVED4[56];__IO uint8_t IP[240]; /*!< Interrupt Priority Register, 8Bit wide */uint32_t RESERVED5[644];__O uint32_t STIR; /*!< Software Trigger Interrupt Register */} NVIC_Type; STM32 的中断在这些寄存器的控制下有序的执行的。
- ISER[8]: ISER 全称是: Interrupt Set-Enable Registers,这是一个中断使能寄存器组。 这里用 8 个 32 位寄存器来控制 ,但是STM32F103 的可屏蔽中断只有 60 个,所以对我们来说,有用的就是两个(ISER[0]和 ISER[1]),总共可以表示 64 个中断。 ISER[0]的 bit0~bit31 分别对应中断 0~31。 ISER[1]的 bit0~27 对应中断 32~59; 你要使能某个中断,必须设置相应的 ISER 位为 1,使该中断被使能(这里仅仅是使能,还要配合中
断分组、屏蔽、 IO 口映射等设置才算是一个完整的中断设置)。具体每一位对应哪个中断,请参考 stm32f10x.h 里面的第 140 行处(针对编译器 MDK5 来说)。
- ICER[8]:全称是: Interrupt Clear-Enable Registers,是一个中断除能寄存器组。 是用来清除某个中断的使能的。其对应位的功能,也和 ICER 一样。这里要专门设置一个 ICER 来清除中断位,而不是向 ISER 写 0 来清除,是因为 NVIC 的这些寄存器都是写 1 有效的,写 0 是无效的。
- ISPR[8]:全称是: Interrupt Set-Pending Registers,是一个中断挂起控制寄存器组。每个位对应的中断和 ISER 是一样的。通过置 1,可以将正在进行的中断挂起,而执行同级或更高级别的中断。写 0 是无效的
- ICPR[8]:全称是: Interrupt Clear-Pending Registers,是一个中断解挂控制寄存器组。其作用与 ISPR 相反,对应位也和 ISER 是一样的。通过设置 1,可以将挂起的中断接挂。写 0 无效。
- IABR[8]:全称是: Interrupt Active Bit Registers,是一个中断激活标志位寄存器组。对应位所代表的中断和 ISER 一样,如果为 1,则表示该位所对应的中断正在被执行。这是一个只读寄存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零
- **IP[240]:全称是: Interrupt Priority Registers,是一个中断优先级控制的寄存器组。**IP 寄存器组由 240 个 8bit 的寄存器组成,每个可屏蔽中断占用 8bit,这样总共可以表示 240 个可屏蔽中断。 STM32 只用到了其中的前 60 个。 IP[59]~IP[0]分别对应中断 59~0。 而每个可屏蔽中断占用的 8bit 并没有全部使用,而是 只用了高 4 位 。 这 4 位,又分为抢占优先级和子优先级。抢占优先级在前,子优先级在后。
STM32 将中断分为 5 个组,组 0~4。该分组的设置是由 SCB->AIRCR 寄存器的 bit10~8 来定义的。具体的分配关系如表 4.5.1 所示:
[tr]组AIRCR[10: 8]bit[7: 4]分配情况分配结果[/tr]
0 | 111 | 0: 4 | 0 位抢占优先级, 4 位响应优先级 | 1 | 110 | 1: 3 | 1 位抢占优先级, 3 位响应优先级 | 2 | 101 | 2: 2 | 2 位抢占优先级, 2 位响应优先级 | 3 | 100 | 3: 1 | 3 位抢占优先级, 1 位响应优先级 | 4 | 011 | 4: 0 | 4 位抢占优先级, 0 位响应优先级 | 我们就可以清楚的看到组 0~4 对应的配置关系,例如组设置为 3,那么此时所有的 60 个中断,每个中断的中断优先寄存器的高四位中的最高 3 位是抢占优先级,低 1 位是响应优先级。每个中断,你可以设置抢占优先级为 0~7,响应优先级为 1 或 0。抢占优先级的级别高于响应优先级。而数值越小所代表的优先级就越高 。
注意两点:第一,如果两个中断的抢占优先级和响应优先级都是一样的话,则看 哪个中断先发生就先执行;
第二,高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的。而抢占优先级相同的中断,高优先级的响应优先级不可以打断低响应优先级的中断。
结合实例说明一下:假定设置中断优先级组为 2,然后设置中断 3(RTC 中断)的抢占优先级为 2,响应优先级为 1。中断 6(外部中断 0)的抢占优先级为 3,响应优先级为 0。中断 7(外部中断 1) 的抢占优先级为 2,响应优先级为 0。那么这 3 个中断的优先级顺序为:中断 7>中断 3>中断 6。
使用库函数实现以上中断分组设置以及中断优先级管理,使得我们以后的中断设置简单化。 NVIC 中断
管理函数主要在 misc.c 文件里面。
- 中断优先级分组函数 NVIC_PriorityGroupConfig ,申明如下:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup); 函数的作用是对中断的优先级进行分组,这个函数在系统中只能被调用一次,一旦分
组确定就最好不要更改。这个函数我们可以找到其实现:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup){assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup));SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup;} 函数唯一目的就是通过设置 SCB->AIRCR 寄存器来设置中断优先级分组, 而其入口参数通过双击选中函数体里面的“IS_NVIC_PRIORITY_GROUP”然后右键“Go to defition of …”可以查看到为 :
#define IS_NVIC_PRIORITY_GROUP(GROUP)(((GROUP) == NVIC_PriorityGroup_0) ||((GROUP) == NVIC_PriorityGroup_1) || ((GROUP) == NVIC_PriorityGroup_2) || ((GROUP) == NVIC_PriorityGroup_3) || ((GROUP) == NVIC_PriorityGroup_4)) 比如我们设置整个系统的中断优先级分组值为 2,那么方法是
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 重要的函数为中断初始化函数 NVIC_Init,确定他的抢占优先级和响应优先级 ,其函数申明 :
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct); 其中 NVIC_InitTypeDef 是一个结构体,我们可以看看结构体的成员变量:
typedef struct{uint8_t NVIC_IRQChannel;uint8_t NVIC_IRQChannelPreemptionPriority;uint8_t NVIC_IRQChannelSubPriority;FunctionalState NVIC_IRQChannelCmd;} NVIC_InitTypeDef; NVIC_IRQChannel:定义初始化的是哪个中断,这个我们可以在 stm32f10x.h 中找到每个中断对应的名字。例如 USART1_IRQn。
NVIC_IRQChannelPreemptionPriority:定义这个中断的抢占优先级别。
NVIC_IRQChannelSubPriority:定义这个中断的子优先级别。
NVIC_IRQChannelCmd:该中断是否使能。
比如我们要使能串口 1 的中断,同时设置抢占优先级为 1,子优先级位 2,初始化的方法是:
NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口 1 中断NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;// 抢占优先级为 1NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;// 子优先级位 2NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能NVIC_Init(&NVIC_InitStructure); //根据上面指定的参数初始化 NVIC 寄存器 中断优先级设置的步骤:
- 系统运行开始的时候设置中断分组。 确定组号,也就是确定抢占优先级和子优先级的分配位数。 调用函数为 NVIC_PriorityGroupConfig();
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口 1 中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;// 抢占优先级为 1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;// 子优先级位 2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
NVIC_Init(&NVIC_InitStructure); //根据上面指定的参数初始化 NVIC 寄存器
中断优先级设置的步骤:
- 系统运行开始的时候设置中断分组。 确定组号,也就是确定抢占优先级和子优先级的分配位数。 调用函数为 NVIC_PriorityGroupConfig();
- 设置所用到的中断的中断优先级别。 对每个中断调用函数为 NVIC_Init();
|
|
2021-11-11 10:45:47
评论
举报
|
|
|