刚开始学32的时候,选择了基于HAL库进行开发,原因是HAL比较容易上手,像点灯、输出PWM、按键输入这种操作都很快捷。但是到ADC+DMA这部分的时候发现,HAL库有一些地方我认为不是很合理和方便。比如DMA中断这部分,ST官方给出的资料里面明确写着HTIE和TCIE这两个中断允许位,用户可以根据自己的需求自行选择DMA搬运数据是搬一半中断一下还是搬完了才中断一次。但在HAL库中,初始化DMA中断,一旦调用HAL_DMA_Start_IT这个DMA中断使能函数,DMA的三个中断(传输过半、传输完成、传输错误)都被使能,虽然可以想要用哪个中断就去哪个回调函数写相应的内容,但是还是让人十分不爽呀。而且这个是CubeMx初始化一次就会把HAL_DMA_Start_IT的内容初始化一次,所以说就算自己到这个函数里面去把不想要的中断给关掉,下次CubeMX生成的时候又会覆盖掉。于是最后我选择放弃HAL库开发,回到标准库开发来。
我是跟着野火的教程一步步来的,写这篇博客单纯为了记录学习路径,方便日后自己看,内容比较简单和粗鄙,让各位见笑了。
第一步–建工程
野火给的库(应该也是官方的库),下载下来解压得到这些文件
里面有用的只有Libraries和Project这两个文件夹里面的内容,Libraries这个文件夹里面放着后面需要使用的标准库,Project这个文件夹里面放着ST官方写的例程,用到芯片的哪个功能如果不会用,打开参考官方的使用方法就可以了。
Libraries这个文件夹里面有
核心文件里面的
CMSIS/CM3 里面有一个CoreSupport文件夹,这个文件夹里面的内容好像也是没用的,ST官方后续好像没有用这个文件夹里面的文件了。主要还是上图这个路径里面的这几个文件,stm32f10x.h这个文件火哥说是最重要的,相当于51单片机里面的reg.51.h文件。下面system_stm32f10x.h应该也是用于初始化时候的,不深究其作用了。然后第一个startup文件夹里面有很多个以.s结尾的启动文件,因为stm32f10有很多个不同的型号,比如103c8t6、103R8T6、103ZET6,这些不同的型号其实底层是一样,但是一些内存容量有区别,所以启动文件应该也是有不同,反正就是根据自己使用的MCU容量大小来选择不同的启动文件。我这里用的是R8T6,对应是中等容量,选择_md后缀的启动文件.
开始新建工程目录(我这里保持跟教程一样)
分为三个文件夹:library(专门用来放库)、Project(专门用来放MDK的工程目录)、User(专门用来放我自己写的代码文件)
library里面内容如下:
就是把上面核心文件里面的东西拷出来了而已,core_cm3.h这个文件应该是没有用的,拷不拷都不影响,然后上面startup文件夹里面放的是所有的启动文件(其实用到的只有这个startup_stm32f10x_md.s)
Project就不说了,MDK新建一个工程的存放路径就在这个文件夹里。
User文件夹里面我放了
这个conf.h 其实就是为了代码简洁,不用在每个文件里面都包含一堆的.h头文件,只要是把conf.h这个头文件一包含,那么就包含了所有的外设类的头文件。算是个中间人一样的东西。
建立好文件夹之后就要去MDK里面建立工程,并且建立工程目录,然后把文件添加进来。
在Target 1右键Add Group添加四个文件夹进来,一个是CMSIS是放核心的.c文件,一个User是放用户写的代码,一个FWLib是放外设那一堆的.c文件,然后Startup文件夹是放启动文件。然后双击这些文件夹,把需要用到的库的所有.c文件选择进来,再把自己定义的main.c之类的用户代码.c文件也添加进来。添加了这些.c文件之后,还要在MDK里面设置这些文件的路径。
这里选的路径是选.h文件的路径,而且要选择到放.h文件的文件夹的那一层路径(就是MDK进入这个文件夹可以看到.h文件),如果选到.h文件的上一层,MDK都会找不到.h文件。
添加完路径之后,这个地方写这两句话
意思是MDK在帮我们编译的时候,后台帮我们编译这两句话,第一句是因为要使用conf.h文件才写的,打开stm32f10x.h文件拉到最下面可以看到:
意思是 如果有 #define USE_STDPERIPH_DRIVER 这一句话,就包含conf.h文件进来,包含了这个中间文件就代表我们包含了所有的外设设备的.h文件进来。
写完USE_STDPERIPH_DRIVER 这句之后 接一个英文的逗号 然后写STM32F10X_MD 意思应该是选择启动文件 反正也是库里面的宏在起作用,如果是大容量,那么这个地方MD应该是要换成别的。我就不细究了。
做完上面这些步骤之后,配置一下仿真器,然后就可以开始写代码了,这个基础环境以后还能复制来用。
第二部–点灯
我在User文件下下新建了一个文件夹(bsp_led:板级支持包_led) 里面放的是bsp_led.c和bsp_led.h,用于存放函数原型和声明文件。
bsp_led.c内容如下:
只有一个初始化led那个IO的初始化函数
#include "bsp_led.h"
void led_gpio_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启相应GPIO口的时钟
//LED接在PA8口上,所以要开GPIOA的时钟,查手册可以查到GPIOA是挂载在APB2时钟总线上的
GPIO_InitTypeDef GPIO_InitStruct; //声明一个结构体,类似int A;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8; //给结构体里面的变量赋初值
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //这些初值应该填什么,可以去查GPIO_InitStruct这个结构体里面有哪些变量,ST官方有写这些变量可以填什么值
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //设置PIN8为推挽输出并设置IO口速度为50Mhz
GPIO_Init(GPIOA,&GPIO_InitStruct); //调用初始化GPIO的函数,第一个形参写GPIO(A-G) 选择自己使用的,第二个形参就是把上面声明的结构体的地址取过来。
//这个函数就初始化了上面我们设置的Pin的工作模式
}
在main.c中:
#include "stm32f10x.h"
#include "bsp_led.h"
void Delay(__IO uint32_t nCount) //简单的延时函数
{
for(; nCount != 0; nCount--);
}
int main(void)
{
led_gpio_init();
while(1)
{
GPIO_SetBits(GPIOA,GPIO_Pin_8); //ST官方给的库函数,将某位置一
Delay(50000);
GPIO_ResetBits(GPIOA,GPIO_Pin_8); //ST官方给的库函数,将某位清零
Delay(50000);
}
}
main函数很简单,首先在进入while循环之前把灯的IO初始化一下,初始化完了之后就可以控制IO的输出状态了,在while循环里面我是让灯闪烁,不过这里的delay延时太短了,肉眼看不出闪烁效果,用示波器是可以看到IO的是一个方波信号。
做到这里点灯就已经成功了。下一步把按键扫描加进来,在User文件夹内新建了一个bsp_key的文件夹,里面存放了bsp_key.c和bsp_key.h两个文件。
按键扫描
bsp_key.c内容如下:
#include "bsp_key.h"
void key_gpio_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_15;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA,&GPIO_InitStruct); //初始化1按键 PA15
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA,&GPIO_InitStruct); //初始化2按键 PA0
}
uint8_t key_scan(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin)
{
if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON) //KEY_ON被宏定义为了0
{
Delay(100000);
if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON)
{
//松手检测
while(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON);//如果一直按下,则一直在这里等待
return KEY_ON; //松手之后就返回一个按键按下的结果
}
else return KEY_OFF; //如果消抖之后判断按键没有按下了,那么还是返回按键没按下的值(KEY_OFF被宏定义为1)
}
else return KEY_OFF;
}
void Delay(__IO uint32_t nCount) //简单的延时函数 注意这里写了之后,main函数的那个要删掉
{
for(; nCount != 0; nCount--);
}
bsp.h的内容如下:
#ifndef __BSP_KEY_H //如果编译时没有包含过bsp.h文件 则执行下面的包含语句 如果已经被包含过了 那么就不再执行这个头文件里面的语句了.
#define __BSP_KEY_H
/*
这个地方多说两句: 这种书写头文件的方法是用于这样的场合 比如main.c里面要包含一下bsp_key.h这个文件
Key_scan.c这个文件里面又包含bsp_key.h这个文件,那么在编译的时候,如果不加#ifndef __BSP_KEY_H
和#endif这种条件编译的语句的话,在编译器编译main.c第一次遇到#include "bsp.h"的时候,
会进入这个头文件把里面的函数和变量声明一次,在后面编译Key_scan.c的时候又遇到#include "bsp.h",
编译器又会把头文件里面的函数和声明又声明一次,就会导致重定义的错误。如果加了条件编译的语句,
编译器第二次进来这个头文件会判断一下这个头文件有没有被包含过,如果有,那么就不再执行下面的声明语句了
*/
#include "stm32f10x.h"
#define KEY_ON 0
#define KEY_OFF 1
void key_gpio_init(void); //按键的GPIO口初始化
void Delay(__IO u32 nCount); //这里声明一下Delay函数,方便给主函数也调用
uint8_t key_scan(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin); //按键扫描函数
#endif
主函数现在是这样的:
#include "stm32f10x.h"
#include "bsp_led.h"
#include "bsp_key.h"
unsigned int i=0;
#define GPIOA_ODR_Addr (GPIOA_BASE+12)
#define PAout(n) *(unsigned int*)((GPIOA_ODR_Addr & 0xF0000000)+0x02000000+((GPIOA_ODR_Addr & 0x00FFFFFF)<<5)+(n<<2))
int main(void)
{
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
led_gpio_init();
key_gpio_init();
while(1)
{
if(key_scan(GPIOA,GPIO_Pin_15) == KEY_ON)
{
PAout(8) = 1;
}
if(key_scan(GPIOA,GPIO_Pin_0) == KEY_ON)
{
PAout(8) = 0;
}
}
}
#define PAout(n)
这句代码是定义位带操作,解释下什么意思:
STM32不能像51单片机那样通过sbit单独定义一个IO,那么如果我想频繁的改变同一组IO下的不同pin,如果使用ST官方给的函数,我就得操作ODR寄存器,比如我要操作GPIOA PIN0为1,GPIOA PIN1为0,那我首先要操作ODR寄存器的第一位&0,然后再操作ODR寄存器第0位|1,这样比较麻烦,尤其是如果需要连续操作同一组IO下的很多个pin。
那么在STM32上,官方为了这种位操作,把SRAM和外设区的低1MB空间内的每一个bit都膨胀成了一个32位的字,拿ODR寄存器来说明一下,意思就是 ODR寄存器里面有32个bit,其中高16位是没用的,低16位中,每一位控制这对应的IO输出状态,比如我要GPIOA PIN0输出1,那我就往ODR寄存器的第0位
写1。位带别名区的意思就是 把ODR寄存器的第0位映射到另一片的内存里,并且这一个bit对应的别名区的地址是32bit的。
那么不难理解,这1MB空间映射到别名区,别名区的大小就是扩大32倍了,所以别名区就是32MB这么大。
野火教程里面有这样一张图
图片比较直观。片上SRAM一般应该很少需要用到位带别名区,因为我们可以定义bit变量。但是外设区的位带别名区还是比较实用的。
计算当前的寄存器的某一位对应的是位带别名区的哪一片地址,是由公式计算出来的,这个公式我没有深究,以后复制来用可以了。
这个程序运行起来就是,按下1按键,led熄灭,按下2按键,led点亮。比较简单。