学习stm32有一个多月了,现在开始整理下思路,最大的感受就是,看起来是在学ARM,实际上是在补51,或者说,学习ARM之前真应该好好学学单片机。或者我根本就不应该分这么清楚,学就是了。都说学过了单片机再学ARM就很容易了,自以为单片机学的不错,想当然的认为ARM就不那么难了,刚开始的时候确实是这么想的,买了开发板,打开做好的工程,修修改改,从点灯开始呗,也没觉得有什么难的,不求甚解,达到目的就行了。过了几天,深入的研究下,仔细看下库函数什么的就不明白了,更不用谈*.s的启动文件了,工程下面那么多文件夹也不知道是什么意思,要自己从0写的话根本无从下手,突然觉得有了51的基础怎么学起来还这么难。难道别个说错了?直到看到这篇帖子:从51到ARM这路怎么走?才恍然大悟,当年学51的时候,写个100多行的程序用矩阵键盘和LCD做个密码锁,觉得还行,然后就一直停在那个阶段了,没什么进步,现在看来只是掌握了最基本的开发流程,并没有真正懂51。一直觉得自己不会用到汇编,所以一遇到汇编就skip,至于c语言,也是一知半解。总之,以前欠的太多了,现在只有补呗。
就从点灯开始,用库做就是这样
Step1 包含*.h头文件
#include "stm32f10x.h"
Step2 定义GPIO初始化结构体
GPIO_InitTypeDef GPIO_InitStructure;
Step3 先定义LED的亮灭,我的芯片是STM32F103VET6,LED接的是PB5。
#define LED_ON GPIO_SetBits(GPIOB, GPIO_Pin_5);
Step4 各种声明,时钟配置、LED配置和延时函数。
void RCC_Configuration(void);
void LED_Config(void);
Step5 各种配置,初始化函数。
//系统时钟配置为72MHZ
void RCC_Configuration(void)
{
SystemInit();
}
//LED 控制初始化函数
void LED_Config(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB| , ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
Step6 终于进main()函数了。
Int main(void)
{
RCC_Configuration(); //系统时钟配置
LED_Config(); //LED控制配置
LED_ON; //LED1亮
while (1)
{
}
}
开发板接好线,编译成功之后load,OK!Led闪烁了。是不是认为做完了,该收工了。其实还没开始呢。分析下每步到底是什么意思,为什么要这样写。
Step1 中为什么要包含#include "stm32f10x.h"这样的一个头文件,有什么用?
Stm32f10x.h是控制器专用头文件,包含了STM32F10X全系列所有外设寄存器的定义(寄存器的基地址和布局)、位定义、中断向量表、存储空间的地址映射等。所以要包含这个文件。
Step2 GPIO_InitTypeDef GPIO_InitStructure;这是什么意思?
GPIO_InitTypeDef是自己定义的一个数据类型(stm32f10x.h):
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
利用关键字typedef和struct,自定义了一个结构体变量,结构体变量的类型名就是GPIO_TypeDef,然后利用这个自定义的数据类型定义了名为GPIO_InitStructure的结构体变量,这个变量在LED 控制初始化函数中将会用到。
Step3中是这样定义led的亮:
#define LED_ON GPIO_SetBits(GPIOB, GPIO_Pin_5);
看下这个函数(在stm32f10x_gpio.c中):
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Pin));
GPIOx->BSRR = GPIO_Pin;
}
传递了两个参数:GPIOx和GPIO_Pin,对应着上面的GPIOB, GPIO_Pin_5, assert_param()这个函数是检查输入参数是否有效,如果无效就产生异常;如果有效,带入参数,执行完GPIOx->BSRR = GPIO_Pin之后就是把PB5设置为1。具体是怎么操作的?先看两个参数的类型,GPIOx是指向GPIO_TypeDef这个自定义结构体类型的指针,类型为指针,指向一个自定义的结构体(在stm32f10x.h中):
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
结构体的类型为自己定义的GPIO_TypeDef。其中__IO和uint32_t是什么东东?
__IO定义在这里(core_cm3.h):
#define __IO volatile
这个宏定义就是用__IO来替换volatile的,那volatile这个关键字又有什么作用。Volatile是不稳定的意思,说明这种类型的值容易发生变化,使用volatile就是不让编译器进行优化,每次读取或者修改值的时候,都必须通过它的硬件地址重新读取或修改,如果不加volatile关键字,编译器就会进行优化,把定义的常量保存在寄存器中,以后就通过访问这个寄存器来访问这个值,因为访问寄存器的速度比访问内存还要快,就达到了优化的效果,但是这个值可能会在外部发生改变,尤其是在嵌入式与硬件相关或者中断的这些地方,它的值在外部改变了之后,装在寄存器中的那个值并没有随着更新,依旧取的是之前优化的时候存下的值,所以取到的就是错误的值了。
__IO:输入输出口,作为输入的时候,当然不能进行优化,它的值随时都会改变,作为输出的时候;也不能优化,优化之后,输出的始终是同一个值,而这个值是会改变的,如果连续两次输出相同值,编译器认为没改变,就会忽略后面那次输出,这就很严重了。
uint32_t定义在这里(stdin.h):
typedef unsigned int uint32_t;
使用typedef定给unsigned int 类型气的别名,因为不同的平台会有不同的字长,所以利用预编译和typedef可以最有效的维护代码。这样很明显的看出是4个字节。
好了,另一个参数自然也就明白了,是2字节的变量了。
再回到这个函数:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
由于GPIO是个指针,GPIO_Pin为常量,所以
#define LED1_ON GPIO_SetBits(GPIOB, GPIO_Pin_5); 预编译之后相当于
GPIOB->BSRR = GPIO_Pin_5;就是把GPIOB的BSRR寄存器设置为0x0020,刚好 第五位置为1,由于是指针变量,所以用“->”直接取自定义结构体GPIOB 中的BSRR 变量的地址,然后赋值为GPIO_Pin_5,在stm32f10x_gpio.c中有这样的定义:
#define GPIO_Pin_5 ((uint16_t)0x0020)
0x0020为0x 0000 0000 0010 0000,然后查STM32参考手册:
把GPIOB的BSRR寄存器的第五位置1,这个寄存器为端口为设置/清除寄存器。
Step4 声明这个文件需要用到的函数,时钟、LED。
Step5
首先是时钟配置,只需要调用系统初始化函数void SystemInit (void)就OK了,在system_stm32f10x.c中,这是微控制器专用系统文件,而函数SystemInit用来初始化为控制器。
然后是LED控制初始化函数,使能APB2口中GPIOB的时钟,设置为通用推挽输出模式,最大输出速度为50MHz,然后向PB5口送数据,调用GPIO_Int函数,OK。(至于推挽模式和最大输出速度为什么这样设置,现在还不是很清楚,研究中···)
Step6 进入main函数,时钟和led配置完了就停在while(1)中,成功点亮led。
现在不用库做,直接操作寄存器。
Step1 定义变量
#define RCC_APB2ENR *(volatile unsigned long *)0x40021018
#define GPIOB_CRL *(volatile unsigned long *)0x40010C00
#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C
Step2 进入main()函数
void main(void)
{
RCC_APB2ENR |= 1<<3;
GPIOB_CRL = (2<<20) | (0<<22);
GPIOB_ODR = 1<<5;
while (1)
{
}
}
保存,编译,load,OK!点亮led。
然后分析下为什么这样做:
Step1 要操作PB5口,首先要使能PB口的CLK,这是stm32特殊的地方,上电时默认外设时钟都是关的;然后是设置PB口的控制寄存器;最后就是送数据。
GPIOB的时钟通过APB2连接,所以应该设置RCC_APB2ENR,查参考手册:
第三位就是PB的时钟使能。然后就是找这个寄存器的地址了。再查memory mapping(stm32f103ve.pdf)基地址就是0x4002 1000,偏移地址:0x18,所以RCC_APB2ENR地址为基地址+偏移 地址:0x4002 1000 + 0x18 = 0x4002 1018。
还可以去查keil给出的头文件\Keil\ARM\INC\ST\STM32F10x\stm32f10x_map.h
#define PERIPH_BASE ((u32)0x40000000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
加上偏移0x18,则RCC_APB2ENR的地址为:
0x4000 0000 + 0x2 0000 + 0x1000 + 0x18 = 0x4002 1018
于是就这样定义了:
#define RCC_APB2ENR *(volatile unsigned long *)0x40021018
0x40021018只是个值,(volatile unsigned long *)进行强制转换,说明这个值是个地址,类型是unsigned long,意思是,读写这个地址时,写入和读出的都是unsigned long类型。加了volatile确保不被编译器优化,每次直接读值。
(volatile unsigned long *)0x40021018是一个指针,不会变,但里面的值容易变,再在前面加“*”,则可以直接操作这个指针指向的地址里面的值,然后就可以直接对这个内存进行读写操作。
剩下的两个用同样的方法找到地址然后定义。
Step2 main()函数
RCC_APB2ENR |= 1<<3;查手册BIT3就是PBEN时钟使能位,置“1”,其他位不管。
GPIOB_CRL = (3<<20) | (0<<22); 将PB5设置为输出,看手册得出 MODE5 是
bit 20 21 控制的,CNF5 是bit 22 23,MODE5应该设置 10(0x2) 选择 2MHZ 输出,CNF5 选择00(0x0),通用推挽模式,于是将这个值写入。
GPIOB_ODR = 1<<5;在相应的ODR为写“1”。
OK!点亮之后停在while(1)中。
好吧,点个灯花了半个月,写个总结又花了2天,真是了解和做出来不是一回事,做出来和写出来也不是一回事,就算写出来了也未必理解的是对的。现在差不多知道该怎么学了,突然发现还有好多东西要学。
网上的资源真是太好了,向这些作者表示感谢:
从51到arm:
http://www.ourdev.cn/thread-5462507-1-1.html
__I、 __O 、__IO是什么意思?
http://blog.csdn.net/denghuanhuandeng/article/details/7281293
uint8_t / uint16_t / uint32_t /uint64_t 是什么数据类型:
http://blog.csdn.net/kiddy19850221/article/details/6655066
*(volatile unsigned long *)的理解:
http://hi.baidu.com/lrbpp/blog/item/8f796d207cded797d0a2d3aa.html