自学STM32的第一天

初学者入门书有没有推荐的

大学学的是单片机89C51,基础不是很好,也就一知半解,能在百度的帮助下修改代码,实现程序功能,下学期大四,因为现在很多公司都用的是嵌入式STM32 招聘要求上写的也是要熟悉STM32和Linux,所以我觉得去认真学习一下STM32,在一开始的时候,很懵圈,直到看到了一本嵌入式入门文章 《零死角玩转STM32》 才有一些了解STM32,下面是书中的语段,想把一些学习到的东西记录下来。
很感谢在网络上无私奉献,分享经验的前辈,虽然我感觉我还没入门,但如果只靠一些书籍,我很难自己理解,下面的文章都是文库里的资料,我只想再学习一次,记录下来,如果谁知道这本书的,和我说一下哪里能买,淘宝上找不到这本书
在这里插入图片描述

一开始通过51来认识STM32

51是嵌入式的入门级的经典MCU,结构简单,易于教学,但是现在市场产品竞争愈发激烈,对成本及其敏感,相应地对MCU的要求也更高了,所以STM32的出现,不是偶然是一种趋势,
本章要结合 《STM32中文参考手册》和《CM3权威指南CnR2》 一起阅读,效果更佳

用寄存器点亮LED

51点亮LED灯

如何用51来点亮一个LED
在硬件上,我们假设在51单片机的P0口的第0位接一个LED,负逻辑亮,代码上,我们会这样写

P0=0XFE;

关掉LED则是

 P0=0XFE;

这里面我们用的是总线的操作方法,即是对P0口的8个IO口同时操作,但起作用的只是P0^0,除了总线操作,我们还学习过位操作,利用51编译器的关键字sbit,我们可以定义一个位变量:

Sbit LED=P0^0;

那么给0亮, 给1灭
*

*LED=0; //亮
    LED=1;//灭**

寄存器

在这里插入图片描述
在点亮LED的时候,我们都是用操作寄存器的方式来实现的,那这个寄存器到底是什么?为什么我们可以直接操作P0口?
STC89C51为例,该单片机主要由51内核,外设IP,总线三大部分组成,内核由Inter公司生产的,总线是用来连接内核和外设的接口单元
寄存器则是内置于各个IP外设中,是一种用于配置外设功能的存储器,就是一种内存,并且有相对应的地址。学过C语言我们就知道,要操作这些内存就可以使用C语言中的指针,通过寻址的方式,来操作这些具有特殊功能的内存-存储器,比如 P0口对应的地址是0X80 ,那么我们需要修改0X80这个地址对应的内存的内容的话,按常理可以这样操作:

*(0X80) =0XFE

可我们这样编译的时候,编译器会报错,在51里面只能通过Sfr和sbit这俩个关键字来是实现寄存器的映像,不能直接操作寄存器对应的地址,这是51不同于STM32的地方
51单片机的这些寄存器位于地址80H—FFH中,对应128个地址,但不是每个地址都是有效的,51有21个,52系列则有26个,其他的都是保留区。

寄存器映射

实际上我们在编程的时候并不是通过指针来操作寄存器的,二十直接给P0,P1这些端口寄存器赋值,那么这些外设资源是如何与地址一一对应的关系,这也得益与51特有的俩个关键字 SFR和SBIT ,其他单片机没有,只能用其他的方式来实现寄存器映射,这俩个关键字帮我们实现了所有寄存器的定义,所以我们才可以像操作普通变量来控制寄存器,所以,我们一开始的点灯LED的代码,全貌应该是

 sfr P0 =0X80;
  P0=0XFE;

为了方便起见,我们可以把寄存器映射全部写好封装在一个头文件里面,不用每用一个寄存器就定义一次,其实这方面的工作,不用我们做,我们在编程的时候都会在开始的地方添加一个头文件:#include<reg51.h>这个头文件已经实现了全部寄存器的定义,该文件是keil自带的
在安装目录Keil \C51\INC 下面可以找到,这个文件实现了字节寄存器和位寄存器的定义
在这里插入图片描述

启动文件-STARTUP.A51

还有一个就是启动代码,这是大多数人容易忽略的地方,我们主要总结一下它的功能,不详细讲解里面的代码
单片机在上电之后,首先执行的是启动文件,而不是我们通常看到的main函数,我们新建51工程的时候会有一个提示,
是否添加启动代码
是否添加启动代码,代码主要实现了以下功能:清除内部数据存储器,清除外部数据存储器,清除外部页储存器,初始化Small模式下的可重入栈和指针,初始化large模式下可重入栈和指针,初始化compact模式下的可冲入栈和指针,初始化8051硬件栈指针,传递初始化全局变量的控制命令或者在没有初始化全局变量时给main函数传递命令,然后程序就跳转到main函数,来到我们熟悉的C语言世界

STM32

现在我们对比51点亮LED的方法,我们先用操作寄存器的方法用STM32点亮一个LED,然后再一步步完善代码,构建最简单的库函数,让我们知道库函数是怎么建立起来的

  1. 启动文件
    新建工程,把工程放入事先建好的文件夹中,然后在工程目录下添加startup_stm32f10x_hd.s
    STM32的启动文件主要实现了:
    1.设置初始SP。
    2.设置初始PC=Reset_Handler
    3.设置向量表入口地址,并初始化向量表。
    4.调用库函数SystemInit ,把系统时钟配置成72M,SystemInit 在库文件system_STM32F10.C定义.
    5.跳转到标号_mian,最终来到C的世界,这里我们先去除繁枝细节,挑重点的讲,主要理解第四和第五点,在启动文件的147—155行,是复位处理函数,代码如下
1 ;Reset handler 
2 Reset_Handler   PROC
3 EXPORT Reset_Handler
4 IMPORT _main
5 IMPORT  SystemInit
6 LDR        R0,=SystemInit
7 BLX        R0
8 LDR          R0,=main
9  BX R0
10 ENDP

简单解释一下这10行代码
第一行 是程序注释,在汇编中注释是“ ;”

第二行,是定义了一个子程序 Reset_Handler ,而 PROC和最后的ENDP 配合使用,是子程序定义伪指令
一般用法
在这里插入图片描述
第三行 EXPORT 表示 该子程序可供其他模块调用
关键字,【WEAK】表示弱定义,如果编译器发现在别处定义了同名的函数,则在连接时用别处的地址进行链接,如果其他地方没有定义,编译器也不报错,以此处地址进行链接

第四行 和 第五行IMPORT 说明SystemInit和_main 这俩个标号在其他文件,在链接的时候需要到其他文件去寻找

SystemInit 在库文件 system_stm32f10x.c实现,用来初始化,STM32的一系列时钟,把系统时钟设置为72MHZ,STM32的时钟比51单片机复杂,需要经过一系列的配置才能达到稳定运行的状态
_main 其实不是我们定义的,当编译器编译时,只要遇到这个标号就会定义这个函数,该函数的主要功能是:负责初始化栈,堆,配置系统环境,并在最后跳转到用户自定义的main函数

第六行 把SystemInit的地址加载到寄存器 R0
第七行 程序跳转到R0中的地址执行程序,之后系统的时钟被设置为72MHZ
第八行把_main 的地址加载到寄存器R0
第九行程序跳转到R0中的地址执行程序,执行完毕之后就去我们熟悉的C世界
第十行表示子程序结束

总结一下,Reset_Handler 这个函数执行了俩个函数的调用,一个是SystemInit ,把系统的时钟设置成72M,令一个_main,初始化系统环境,最终调用C的main
**等会我们的主要任务还是点亮 LED 的时候采用最简单的方法,直接使用内部的LSI时钟,(8MHZ)作为主时钟即可,不使用外部时钟LSE **
_main 函数由编译器生成,负责初始化栈,堆等,,并在最后跳转到用户自定义的main函数

  1. mian.c
    先编写一个 main 函数
    在这里插入图片描述
    这时候出现了一个错误
Undefined symbol SystemInit (referred from startup_stm32f10x_hd.o)

SystemInit 没有定义,从分析启动文件时,我们知道,Reset_Handler 调用了该函数用来初始化系统时钟,而该函数实在库文件 System_stm32f10x.c中实现的,我们重新写一个函这个的函数也可以,把功能完整的实现一边,但为了方便起见,我们在main文件里面定义一个SystemInit空函数,为的是骗过编译器,把这个错误去掉,关于配置系统时钟我们在后面再写简单的代码
在这里插入图片描述
这时候编译没有错了,还有一个方法是再启动文件中把关于SystemInit函数的代码注释掉也可以,代码如下:
在这里插入图片描述
4. 控制IO口
下面我们从三个方面来讲解STM32的IO口在控制LED时,和51的区别,关于STM32DE IO寄存器的介绍,我们可以看《STM32中文参考手册》第八章即可,下面涉及的IO寄存器均来自这一章的第二小节

寄存器

1.电平控制

51单片机的IO口如果要输出1和0,可以直接赋值,不用控制其他寄存器
而STM32的IO口比较复杂,如果要输出1和0,则要通过控制;端口输出数据寄存器ODR来实现 ,ODR 是 Output data register 的简写,在STM32里面,其寄存器的命名都是英文的简写,很容易记住,从手册上我们知道ODR是一个32位的寄存器,低16位有效,高16位保留,低16位对应着IO 0- IO 16 ,只要往对应的位置写入0或者1 就可以输出低或者高电平

在这里插入图片描述
PB0输出低电平,代码如下

GPIOB_ODR  = 0<<0

这时候编译,会有一个错误,说 GPIOB_ODR 没有定义,不过我们确实没有定义,在51单片机中,我们可以直接往P0口赋值,那是因为在reg51.h这个头文件里面实现了P0口这个寄存器的映像,用的是51特有的SFR来实现的,但是STM32不一样没有,sfr,所以只能用其他的方式来实现寄存器映像,因为寄存器实际上就是具有特殊功能的内存,那么我们可以通过宏定义来实现寄存器映像,其实ST的库函数中用的也是这种方法,从手册中我们看到ODR寄存器的偏移是:0CH,(上图左上角),这个偏移地址是基于端口的起始地址而言的,在STM32中,每个外设都有一个起始地址,叫外设基地址,外设的寄存器,就以这个基地址位标准按照顺序排列,跟结构体里的成员差不多。
在手册的第二张,2.3 存储器映像中,可以查找所有外设的基地址,如下在这里插入图片描述
其中 GPIOB的起始地址位 : 0X40010C00 ,从这个地址,我们可以算出GPIOB_ODR寄存器的地址位:0x40010C0C
0X4001 0C00 + (地址偏移)0X 0C =(寄存器地址)
所有定义代码如下:

#define GPIO_ODR  *(volatile unsigned long *) 0x40010C0C

long是32位整型,unsigned指无符号数,左边的*表示取内容
volatile表示易变的,告诉编译器不要优化,这个地址的内容不一定是在程序中改变的。
volatile unsigned long * 表示将后面跟的内容转化成一个指针,并且是指向一个易变的无符号整数。
左边再加个 * ,表示取该指针指向地址的内容。
总的意思是取那个内存单元(内存地址0x40010C0C)里存的数,并将这个数转化为无符号整数

所以有了这个寄存器定义,我们就可以直接控制操作FPIOB_ODR了

2.方向控制

在这里插入图片描述
虽然配置了ODR,但这个时候还不能点亮LED,因为STM32的IO口还要配置方向,这个由端口配置寄存器来控制,端口配置寄存器分为高低俩个,每4bit控制一个IO口,所以端口配置寄存器:CRL控制这个IO口的低8位,端口配置高寄存器: CRH控制这个IO口的高8bit,在4位一组的控制位中,CNFy【1:0】用来控制端口的输入输出,MODEy【1:0】用来控制输出模式的速率,即输出时,IO电平翻转的速度
输入有三种模式。输出有四种模式,我们在控制LED的时候,选择通用推挽输出。
输出速率有三种模式:2M,10M,50M,这里我们选择2M
同GPIOB_ODR 一样,我们也可以算出FPIO_CRL的地址为:0X40010C00 那么设置PB0为通用推挽输出,输出速率为2M的代码则如下
#define GPIOB_CRL *(volatile unsigned long *) 0x40010C00 //配置PB0位通用推挽输出,输出速率为2M GPIOB_CRL = 2<<0 | 0<<0
这个代码我又查了了一下

  GPIOB->CRL = 0x00000002;
   //PB0:CNF0[1:0]=00, MODE0[1:0]=10

这样就好理解多了,主要还是要学会看手册

3.时钟控制

A
在这里插入图片描述
当我们设置了IO口的方向,并在相应的输出寄存器里输入了值得时候,觉得现在总算可以点亮LED了吧,其实还差最后一步
STM32外设很多,未来降低功耗,每个外设都对应了一个时钟,在系统复位的时候这些时钟都是关闭的,想要这些外设工作,必须把相应的时钟打开
而STM32的所有外设的时钟由一个专门的外设来管理叫RCC(reset and clock contrlo) 在手册第六章
STM32的外设因为速率的不同,分别挂载在三条总线上,AHB,APB2,APB1,AHB位高速总线,APB2次之,APB1再次之,所有的IO口都挂载在APB2总线上,属于高速外设,时钟有APB2外设时钟使能寄存器 (RCC_APB2ENR )来控制,其中PB端口的时钟由该寄存器的位3写1使能。
同ODR 和CRL ,我们可以算出RCC_APB2ENR的地址
0x40021018

#define RCC_APB2ENR  *(volatile unsigned long *) 0x40021018
   //开启端口B
    GPIOB_CRL = 1<<3;
 手册上位3为 B端口使能端 给1 使能

如果你足够细心,你会发现我们虽然开了端口时钟,那么这个时钟到底多大呢?又是用哪里来的呢
如果我们用的是库,那么会有一个库函数 SystemInit,会帮我们把系统时钟设置位72M 但现在我们没有使用库,那现在时钟是多少呢?答案是8M,当外部HSE没有开启或者出现故障的时候,系统时钟由内部低速时钟LSI提高,现在我们确实没开启HSE,所有系统默认的时钟位LSI=8M,至于更深入的细节,在后面的RCC时钟树中在详细分析,如果你想自己先尝尝鲜,那么可以看RCC外设中的:时钟控制寄存器 (RCC_CR)和时钟配置寄存器(RCC_CFGR)这俩个寄存器即可

4.水到渠成

我们现在控制了电平,配置了方向,开启了时钟,经过这三部,我们总算可以控制一个LED了,比起51直接输出点评,控制STM32的IO多了俩步:即配置方向可开启时钟,比起AVR和PIC这俩种单片机则多了开启时钟这一步

下面是一个完整的用STM32控制一个LED代码

 #define RCC_APB2ENR *(volatile unsigned long *)0x40021018
 #define GPIOB_CRL   *(volatile unsigned long *)0x40010C00
 #define GPIOB_ODR   *(volatile unsigned long *)0x40010C0C
 int main (void)
 {
    //开启端口B 的时钟
	RCC_APB2ENR |= 1<<3;
	//配置PB0位输出速率为2M,通用推挽输出
	GIOPB_CRL  |=(2<<0)|(0<<2);	   
	//PB0输出低电平,点亮LED
	GIOPB_ODR = 0<<0 ;
 }
 void SystemInit (void)
 {

 }

再接再厉

学习STM32存在一个用库好还是用寄存器好的争议,就好像编程用汇编好还是用C好一月,其实孰优孰劣,在市场上自有定论,用户群说明一切
虽然我们上面用寄存器点亮了LED,乍一看好像代码也很姜丹,但是我们别侥幸以后就一直用寄存器开发了,在用寄存器点亮LED的时候,我们是否发生STM32的寄存器都是32位的,在配置时非常容易出错,而且代码还很不好理解,所以学习STM32最好的方法是用库,然后在库的基础上了解底层,看遍所有寄存器
这里我们讲一下,关于GPIO库,其他的外设我们直接参考库学习即可,不必自己写

定义外设寄存器结构体

上面我们在操作寄存器的时候,操作的是寄存器的绝对地址,如果每个寄存器都这样操作的话,那将非常麻烦
我们考虑到外设寄存器的地址都是基于外设地址的偏移地址,都是在外设基地址上逐个增加的,每个寄存器占了32个或16个字节,这种方式和结构体里面的成员类似,所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一月,这样我们操作寄存器的时候,就不用每次都找到绝对地址,只要找到外设的基地址就可以操作外设的全部寄存了
下面我先定义一个GPIO 寄存器结构体,结构体里的成员全是GPIO的寄存器,按寄存器的偏移地址从低到高排列

//GPIO 寄存器结构体
 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_TypDef;

在手册 第8.2我们可以找到7个寄存器描述,在点亮LED的时候,我们只用了CRL和ODR这俩个寄存器,至于其他寄存器功能,自行看手册
在GPIO结构体中,我们用来俩个数据类型,一个是 uint32_t ,表示无符号的32位整型,因为GPIO的寄存器都是32位的,这个类型声明在标准头文件stdint.h里面,我们在程序上只要包含这个头文件即可
另外一个是_IO,这个是我们自己定义的,原型是volatile ,作用是高速编译器不要因优化而省略此指令,必须每次都直接读写其值,这样就可以确保每次读写或者写寄存器都真正执行到位
为了这俩个数据类型,我们添加一下代码

#includu <stdint.h>
#define _IO  volatile

外设声明

寄存器结构体已经定义好了,GPIO端口分为A-G,每个端口都含有GPIO_TypDef 类型的指针,然后我们就可以根据端口名(实际上现在是结构体指针)来操作各个端口的寄存器,代码实现如下:

//GPIO端口及地址
 #define GPIOA_BASE  (APB2PERIPH_BASE+0X0800)
 #define GPIOB_BASE  (APB2PERIPH_BASE+0X0C00)
 #define GPIOC_BASE  (APB2PERIPH_BASE+0X1000)
 #define GPIOD_BASE  (APB2PERIPH_BASE+0X1400)
 #define GPIOE_BASE  (APB2PERIPH_BASE+0X1800)
 #define GPIOF_BASE  (APB2PERIPH_BASE+0X1C00)
 #define GPIOG_BASE  (APB2PERIPH_BASE+0X2000)

对于其他外设,我们也可以这样将外设的名字定义为一个外设寄存器结构体类型的指针,这里我们只讲GPIO

在这里插入图片描述

这里我蛮奇怪的第一个地方,还在探索,为什么GPIO端口F和G的起始位置一样

APB1,APB2,AHB 总线基地址

在这里插入图片描述
这张图在之前有,只是我拿出来说一下,总线的位置在这里然后现在先定义基地址,然后在基地址上加入偏移地址就可以

在这里插入图片描述
直接上图
在这里插入图片描述
在这里插入图片描述

#include <stdint.h>	  //点一个LED灯
#include <stm32f10.h>
#define _IO volatile
//GPIO 寄存器结构体
 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_TypDef;
 //RCC 寄存器结构体
 typedef struct(
  _IO uint32_t CR;
  _IO uint32_t CFGR;
  _IO uint32_t CLR;
  _IO uint32_t APB2RSTR;
  _IO uint32_t APB1RSTR;
  _IO uint32_t AHBENR;
  _IO uint32_t APB2ENR;
  _IO uint32_t APB1ENR;
  _IO uint32_t BDCR;
  _IO uint32_t CSR;
 ) Rcc_TypDef;

  //总线基地址
 #define PERIPH BASE  ((uint32_t)0x40000000)		   //外设基地址
 #define APB1PERIPH_BASE  PERIPH_BASE
 #define APB2PERIPH_BASE  (PEPIPH_BASE+0X10000)
 #define AHBPERIPH_BASE  (PEPIPH_BASE+0X20000)
//GPIO端口及地址
 #define GPIOA_BASE  (APB2PERIPH_BASE+0X0800)
 #define GPIOB_BASE  (APB2PERIPH_BASE+0X0C00)
 #define GPIOC_BASE  (APB2PERIPH_BASE+0X1000)
 #define GPIOD_BASE  (APB2PERIPH_BASE+0X1400)
 #define GPIOE_BASE  (APB2PERIPH_BASE+0X1800)
 #define GPIOF_BASE  (APB2PERIPH_BASE+0X1C00)
 #define GPIOG_BASE  (APB2PERIPH_BASE+0X2000)
 //RCC
 #define RCC_BASE ( AHBPERIPH_BASE+0X1000)
//外设声明 
 #define GPIOA ((GPIO_TypeDef*) GPIOA_BASE)
 #define GPIOB ((GPIO_TypeDef*) GPIOB_BASE)
 #define GPIOC ((GPIO_TypeDef*) GPIOC_BASE)
 #define GPIOD ((GPIO_TypeDef*) GPIOD_BASE)
 #define GPIOE ((GPIO_TypeDef*) GPIOE_BASE)
 #define GPIOF ((GPIO_TypeDef*) GPIOF_BASE)
 #define GPIOG ((GPIO_TypeDef*) GPIOG_BASE)
 #define RCC   ((RCC_TypeDef*)    RCC_BASE) 

 //
 #define RCC_APB2ENR *(volatile unsigned long *)0x40021018
 #define GPIOB_CRL   *(volatile unsigned long *)0x40010C00
 #define GPIOB_ODR   *(volatile unsigned long *)0x40010C0C

 int main (void)
 {
    //开启端口B 的时钟
	RCC->APB2ENR |= 1<<3;
	//配置PB0位输出速率为2M,通用推挽输出
	GIOPB->CRL  |=(2<<0)|(0<<2);	   
	//PB0输出低电平,点亮LED
	GIOPB->ODR = 0<<0 ;
 }
 void SystemInit (void)
 {

 }

这里我们用的是宏定义,之前我们用的是结构体定义,所以有没有看出来在main函数的代码有一些不一样

用结构体可以直接操作,用宏定义还要一个个的找到寄存器的绝对地址进行重新定义

比如开时钟
RCC->APB2ENR |= 1<<3;
这章节本来后面还有一些是关于软件的实际操作的,但是因为刚刚学还没买开发板,学生一名,想多看看,对比一下,找一下性价比高的开发板,如果有这本书的,电子版的或者实体销售店的··留言一下, 这本书第一次看的时候感觉刚好把我学的51和想学的STM32结合在一起,更好理解了

上面是我自己选重点给自己看的
https://wenku.baidu.com/view/193e26de900ef12d2af90242a8956bec0975a58c.html 原文件
指针的概念理解
https://www.cnblogs.com/Waming-zhen/p/4353963.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值