STM32基础

时间:2024-03-06 12:03:32

目录:

1、STM32简介

2、STM32分类

3、STM32 IC的命名方式

4、STM32容量的划分

5、STM32 IO简介

6、STM32的BOOT配置

7、STM32基础知识

8、存储器映射

9、寄存器映射 

 

1、STM32简介

  STM32中的ST指的是意法半导体,M是Microelectronics的缩写,32表示32位,即意法半导体公司开发的32位微控制器。

  微控制器和微处理器的区别:

    微处理器有MMU即内存管理单元,且主频比较高,资源比微控制器丰富。比如ARM9就属于微处理器。

2、STM32分类

  STM32有不同内核的IC,如下图所示:

3、STM32 IC的命名方式

  STM32 IC的命名方式如下图所示:

  拿到一颗IC之后,可以通过这个图来确认IC的管脚数、封装类型、FLASH容量、工作温度范围等参数。

  比如用STM32F103ZET6来说明:

    • STM32表示是32位的MCU。
    • F表示基础类型。
    • Z表示144个脚位。
    • E表示FLASH容量为512K。
    • T表示QFP封装。
    • 6表示温度等级为A:-40~85℃。

  其它IC的可以根据命名图来查看。

4、STM32容量的划分

  STM32的IC可以通过容量的不同进行划分,有小容量、中容量和大容量之分。还有一些互联型产品。

  小容量IC指的是FLASH容量在16KByte到32KByte。

  中容量IC指的是FLASH容量在64KByte到128KByte。

  大容量IC指的是FLASH容量在256KByte到512KByte。

  ST针对不同的IC提供了不同的启动文件,不同容量的IC所使用的启动文件也是不一样的。如下:  

  带有hd(High Dedsity)后缀的是大容量芯片的启动文件。

  带有md(Medium Dedsity)后缀的是中容量芯片的启动文件。

  带有ld(Low Dedsity)后缀的是小容量芯片的启动文件。

  有一些文件还带有vl、cl、xl等后缀,区别如下:

  cl:connective line 指的是互联型产品stm32f105xx和stm32f107xx系列。

  xl:extreme line 超高密度型产品,这种应该是指stm32f101xx和stm32f103xx容量在MB级别的产品。

  Vl:valuable line超值型产品,这种是指stm32f100系列。

  也可以通过IC的名称来区分是那种容量的IC,比如:

  STM32F103x4和STM32F10x6是小容量的产品,不同的地方是x4和x6,不知道是否是x4表示16Kbyte,x6表示32KByte,这个可以自己查下。

  STM32F103x8和STM32F10xB是中容量的产品,不同的地方是x8和xB,不知道是否是x8表示64Kbyte,xB表示128KByte,这个可以自己查下。

  STM32F103xC、STM32F10xD和STM32F10xE是大容量的产品。不同的地方是xC、xD和xE。

5、STM32 IO简介

  STM32引脚分类如下:

    1. 电源脚:VBAT、VDD、VSS、VDDA、VSSA,VREF+、VREF-等。其中VBAT是备用电源;VDD和VSS是数字电源;VDDA和VSSA是模拟电源;VREF+和VREF-是参考电源。
    2. 晶振脚位:主晶振IO、RTC晶振IO。
    3. 下载脚位:用于JTAG下载的IO:JTMS、JTCK、JTDI、JTDO、NJRST。
    4. BOOT脚位:BOOT0和BOOT1,用于设置系统的启动方式。
    5. GPIO脚位:专用器件接到专用总线,如I2C、SPI、SDIO、FSM、DCMI等总线需要接到专用的IO。根据实际使用的GPIO功能进行连接即可。

  其中1、2、3、4这几个部分IO组成的系统可以称之为最小系统,也就是芯片运行的最小外围。       

  具体的STM32 IC的脚位内容可以查看参考手册和数据手册。

  参考手册和数据手册的区别如下:

    • 参考手册:  

  主要内容为:片上外设的功能说明和寄存器描述;这个手册对片上的每一个外设的功能和使用做了详细的说明,包含寄存器的介绍。

    • 数据手册:

  主要讲这个芯片有哪些功能、详细描述每一个引脚的功能、介绍该芯片的内存映射,列举了每个总线的地址和外设、芯片封装的介绍。数据手册主要是简单的介绍。

  通过以上介绍可以看出数据手册主要是简单的介绍,在芯片选型和原理图设计的时候只要看数据手册就可以了;而参考手册是详细介绍芯片资源和寄存器的,在写程序的时候有不明白的就可以查看参考手册。

  打开STM32F103xx的数据手册,在引脚定义中可以看到如下图所示:      

 

  以下是该图片的序号说明:

1、引脚序号:在Pins下面有不同的封装,每个封装的引脚序号有可能不同,需要根据实际使用的封装来对照脚位。

2、引脚名称:是指在复位状态下的引脚名称。

3、引脚类型:

      • S:电源引脚。
      • I:输入引脚。
      • I/O:输入/输出引脚。

4、I/O结构:

      • FT:兼容5V。
      • TTa:只支持3.3V,且直接到ADC。
      • B:BOOT引脚。
      • RST:复位引脚,内部带弱上拉。

5、主功能:每个引脚复位后的功能。

6、复用功能:这里指的是IO的默认的复用功能。

7、重映射功能:IO除了默认的复用功能之外,还可以通过重映射的方法映射到其它的IO,这样就增加了IO口功能的多样性和灵活性。

6、STM32的BOOT配置

  将程序下载进STM32的芯片,重启芯片后,在系统时钟的第4个上升沿,BOOT0和BOOT1引脚的值将被锁存,也就是说BOOT0和BOOT1的高低电平状态在上电的时候被记录了。

  STM32有三种启动模式,芯片会根据BOOT0和BOOT1引脚的状态来选择启动模式,如下图所示:

  1、主闪存存储器启动模式:

  主闪存存储器就是FLASH,编写好的程序都是下载到FLASH当中,在这个模式下启动芯片,就是在FLASH的起始地址开始运行程序的。

  2、系统存储器:          

  该模式启动的程序功能是由厂家设置的。这种启动方式用的比较少。系统存储器是芯片内部一块特定的区域,芯片在出厂是,厂商在这个区域内部预置了一段引导启动程序,也就是俗称的BootLoader,这个一块固定的ROM,出厂后无法修改。  

  使用系统存储器启动芯片是为了从串口下载程序,在厂家提供的BootLoader中,提供了串口下载程序的固件,运行BootLoader程序,可以通过串口将程序下载到FLASH中,下载步骤如下:

    • 将BOOT0设置为1、BOOT1设置为0,然后复位启动芯片,使芯片在系统存储器中启动。
    • 在系统存储器的BootLoader已将将串口设置好,只要通过串口下载软件将程序下载到FLASH中就好。
    • 程序下载完之后,将BOOT0设置为0,然后复位启动芯片,这样芯片就烧录好了。

  3、内置SRAM:

  内置SRAM,SRAM是没有程序存储功能的,断电就丢失了。用这个模式启动程序一般用于调试程序。一般如果将程序下载到FLASH中会比较费时,在SRAM中调试代码比较快,但是SRAM断电就会丢失数据,所以只适合调试用。

  FLASH锁死后的解决办法

  在调试IC的过程中,某些误操作可能会导致内部FLASH锁死,连接SWD和JTAG也无法读取到设置,没办法下载程序。这个时候就可以通过修改BOOT模式进入系统存储器,通过BootLoader程序使用串口下载程序到FLASH中解决锁死的问题。  

  一键ISP下载程序:

  ISP(In System Programming)指在系统可编程,指空白IC可以写入用户代码。已经变成的器件也可以用ISP方式擦除或再变成。

  ISP下载程序的时候需要用到BootLoader程序,也就是说需要通过BOOT0和BOOT1使IC在系统存储器中启动,通过一种可用的串行外设(USART、CAN、USB、I2C等)将应用程序下载到内部FLASH中。

  以USART1的ISP进行分析,通常ISP下载的步骤如下:

      1. 电脑通过USB转串口线连接到STM32的USART1,并打开电脑端的上位机。
      2. 设置BOOT0为高电平,BOOT1位低电平。
      3. 复位芯片使其进入BootLoader模式,然后通过上位机下载程序。
      4. 下载完后,设置BOOT0和BOOT1都为低电平。
      5. 复位芯片,运行代码。

  以上步骤如果每次都要设置BOOT0和BOOT1的状态,比较繁琐,有一种一键ISP下载可以解决这种繁琐,在网上也可以查到资料。这里截取一种来说明,其原理图如下:

  USB转串口,一般用到RXD和TXD这两个口,一键ISP下载电路中需要用USB转串口的芯片的DTR口和RTS口来控制单片机的BOOT0和NRST,BOOT0为0则从内部FLASH启动,BOOT0为1则从系统存储器启动。

    原理如下所示:

      1. 通过上位机控制USB转串口芯片的RTS脚位低电平,Q1导通,BOOT0的电平上拉为高电平,使MCU进入系统存储器模式。
      2. 通过上位机控制USB转串口芯片的DTR脚为高电平,由于RTS脚为低电平,Q2导通,U18对的2脚为低电平,U18为一个模拟开关,使能端由4脚控制,默认高电平,U18的1脚和2脚导通,所以NRST为低电平系统复位。
      3. 单片机进入ISP模式,此时可以将DTR脚设置为低电平,RTS设置为高电平。Q1和Q2位截止状态,BOOT0和NRST还原默认电平。
      4. 上位机将程序下载到单片机,下载完毕之后,程序自动运行。

    ISP的串口下载软件如下图所示:

 

7、STM32基础知识

  STM32芯片主要有内核和片上外设组成,与电脑类比,芯片的内核和外设就如同电脑上的CPU与主板、内存、显卡、硬板的关系。

  STM32不同的芯片型号采用的内核是不一样的,内核即CPU,CPU由ARM公司设计。ARM公司并不生产芯片,而是出售其芯片技术授权。芯片生产厂商如ST、TI、Freescale负责内核之外的部件设计并生产整个芯片,这些内核之外的部件就称为外设。如GPIO、USART、I2C、SPI等都叫做片上外设。

  STM32芯片架构图如下:

  从中可以看出内核与外设之间通过各种总线连接。

  STM32Ff10xx的系统框图如下:

  从图中可以看出其有4个驱动单元、4个被动单元。可以将驱动单元理解成是内核部分,被动单元理解成外设。  

  • ICode总线:

  ICode总线是指指令总线。程序编译之后成为了一条条指令,将程序下载到芯片中,就是将这些指令存放到FLASH当中,内核通过ICode总线读取存放在FLASH当中的指令。ICode几乎每时每刻都需要被使用,它专门用来取指。  

  • DCode总线

  DCode总线是指数据总线。在编写程序的时候,数据有常量和变量两种类型,常量是指固定不变的数据,在C语言中用const关键字修饰,存放在FLASH中。变量是指可变的,不管是全局变量还是局部变量都存放在SRAM中。读取SRAM中的数据就是通过DCode总线实现的。因为数据可以被DCode总线和DMA总线访问,所以为了避免访问冲突,在取数据的时候需要经过一个总线矩阵来仲裁,决定哪个总线正在取数。 

  • 系统总线

  系统总线主要是访问外设的寄存器,即在读写寄存器是通过系统总线完成的。

  • DAM总线

  DAM总线也主要是用来传输数据,这个数据可以是在某个外设的数据寄存器,可以在SRAM,可以在内部的FLASH。因为数据可以被Dcode总线和DAM总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵仲裁,决定哪个总线正在取数。

  • 内部的闪存存储器

  内部的闪存存储器即FLASH,这是存放程序的地方。内核通过ICode总线来取里面的指令。

  • 内部的SRAM

    内部的SRAM,即RAM,程序的变量、堆栈等的开销都是基于内部的SRAM。内核通过DCode总线来访问它。

  • FSMC

  FSMC的英文全称是Flexible static memory controller,叫灵活的静态的存储器控制器,是STM32F10xx中一个很有特设的外设,通过FSMC,可以扩展内存,如外部的SRAM,NANDFLASH和NORFLASH。但有一点需要注意的是,FSMC只能扩展静态的内存,即名称里面的S:static,不能是动态的内存,比如SDRAM就不能扩展。

  • AHB到APB桥

  从AHB总线延伸出来的两条APB2和APB1总线,上面挂载着STM32各种各样的特设外设。如GPIO、串口、I2C、SPI这些外设就挂载在这两台总线上。

8、存储器映射

STM32的所有功能部件都排列在一个4GB(2^32大小)的地址空间之内。在编程的时候,可以通过它们地址读写 。

STM32存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配的,给存储器分配之地的过程就称为存储器映射,如果给存储器再分配一个地址就叫存储器重映射。

STM32的存储器地址映射图如下: 

  从图中可以看出STM32的地址是从0x00000000~0xFFFFFFFF,总的地址就是4GB。在这4GB的地址内具体分配什么内容是由芯片厂商决定的,这样就造成了不同芯片厂商生产的芯片的功能是有差异的。

  存储器区域划分:

  从存储器映射图中可以看出,4GB的地址空间被划分成了8个块,每个块的大小为512MB(4GB=8*512MB),每个块实现不同的功能。 

存储器功能分类图如下:

  在这8个Block里面,Block0用来设计成内部FLASH;Block1用来设计成内部RAM;Block2用来设计成片上外设。

  Block3、Block4、Block5都是给FSMC使用的。

  Block6没有使用,具有要查看具体的芯片使用情况。

  Block7是给内核外设的,比如NIVC和SYSTICK等外设。有一些外设是在内核当中的,查看的时候需要跟外设区分开。

  存储器Block0的内部区域功能划分:     

  Block0主要用于设计片内的FLASH,不同型号的芯片的FLASH大小是不一样的,但是它们的起始地址是一样的,都是从Block0的地址0x08000000开始,结束地址决定了芯片的FLASH大小,Block0内部区域功能划分如下图所示:           

      从图中还可以看出之前提到的系统存储器也是位于Block0之中。

  存储器Block1的内部区域功能划分: 

  Block1用于设计片内的SRAM,跟FLASH一样,不同芯片的SRAM的大小也是不一样的,但是SRAM的起始地址都是0x20000000,而结束地址就决定了SRAM的大小。Block1内部区域功能划分如下图:     

存储器Block2的内部区域功能划分:

  Block2主要用于设计片内的外设,根据外设的总线速度的不同,Block2被分成了APB和AHB两部分,其中APB又被分为APB1和APB2,如下图所示:

9、寄存器映射      

  存储器本身没有地址,给存储器分配地址的过程叫存储器映射。在存储器Block2这块区域,设计的是片上外设,它们以四个字节为一个单元,即一个单元有32bit,每个单元对应不同的功能,当控制这些单元时就可以驱动外设工作。

  在编程的时候找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,为了可以更简洁方便,可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。

  比如,找到GPIOB端口的输出数据寄存器ODR的地址是0x40010C0C,ODR寄存器32bit,低16bit有效,对应着16个外部IO,写0/1对应的IO则输出低/高电平,通过C语音指针的操作方式,让GPIOB的16个IO都输出高电平,如下: 

  *(unsigned int *)(0x40010C0C) = 0xFFFF; 

   0x40010C0C对于我们来说是GPIOB端口ODR寄存器的映射地址,但是对于编译器来说,0x40010c0C只是一个普通的常量,是一个立即数,要让编译器认为这是GPIOB端口的ODR寄存器地址,就需要进行指针操作,将立即数强制转换为指针,如上面代码的操作,(unsigned int *)(0x40010C0C)就是将立即数0x40010c0c强制转换为unsigned int的指针类型,然后再用指针符号*对这个转换后的指针进行操作。这样就是操作了GPIOB端口的ODR寄存器。

  通过上面的操作可以看到,用立即数进行操作比较繁琐,可以通过#define对立即数进行变换,如下:

  #define   GPIOB_ODR   (unsigned int *)(0x40010C0C)
  *GPIOB_ODR = 0xFFFF;   

   为了更方便,可以将*GPIOB的*指针符号也放到#define当中,如下:

  #define    GPIOB_ODR    *(unsigned int *)(0x40010C0C)
  GPIOB_ODR = 0xFFFF;

封装总线和外设基地址

  外设总线基地址如下图:

  IC的外设一般挂载在总线上。IC外设的寄存器的地址都是根据总线基地址偏移得来的。

  在编程上为了方便理解和记忆,可以把中心基地址和外设基地址都以相应的宏来定义,总线或者外设都以它们的名字作为宏名,如下:

   /* 片上外设基地址 */
  #define    PERIPH_BASE    ((unsigned int)0x40000000)
 
  /* 总线基地址 */
  #define    APB1PERIPH_BASE    PERIPH_BASE
  #define    APB2PERIPH_BASE    (PERIPH_BASE + 0x00010000)
  #define    AHBPERIPH_BASE     (PERIPH_BASE + 0x00020000)

  /* 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)
 
  /* 寄存器基地址,以 GPIOB 为例 */
  #define    GPIOB_CRL    (GPIOB_BASE+0x00)
  #define    GPIOB_CRH    (GPIOB_BASE+0x04)
  #define    GPIOB_IDR    (GPIOB_BASE+0x08)
  #define    GPIOB_ODR    (GPIOB_BASE+0x0C)
  #define    GPIOB_BSRR   (GPIOB_BASE+0x10)
  #define    GPIOB_BRR    (GPIOB_BASE+0x14)
  #define    GPIOB_LCKR   (GPIOB_BASE+0x18)

  在上面的代码中,首先定义了“片上外设基地址”PERIPH_BASE,接着在PERIPH_BASE上加入各个总线的地址偏移,得到APB1、APB2和AHB总线的基地址APB1PERIPH_BASE、APB2PERIPH_BASE和AHBPERIPH_BASE。

    得到总线的基地址之后,就可以通过总线基地址+偏移地址得出具体外设的基地址,比如GPIOA外设时挂载在APB2总线上的外设,GPIOA外设的基地址为0x40010800,也就是APB2总线基地址+0x00010800。

    一旦得出了具体的地址之后,就可以通过指针操作外设的寄存器了,如下代码:
  /* 控制 GPIOB 引脚 0 输出低电平(BSRR 寄存器的 BR0 置 1) */
  *(unsigned int *)GPIOB_BSRR = (0x01<<(16+0));

  /* 控制 GPIOB 引脚 0 输出高电平(BSRR 寄存器的 BS0 置 1) */
  *(unsigned int *)GPIOB_BSRR = 0x01<<0;
 
  unsigned int temp;
  /* 读取 GPIOB 端口所有引脚的电平(读 IDR 寄存器) */
  temp = *(unsigned int *)GPIOB_IDR;

  该代码使用(unsigned int *)把GPIOB_BSRR宏的数值强制转换成了地址,然后再用“*”号做取指针操作,对该地址赋值,从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取STM32外设的状态。

 封装寄存器列表

  STM32的外设很多,如果每个外设的寄存器都听过宏定义去实现的话,会很繁琐。其实STM32很多外设是成组出现的,比如GPIO口,从GPIOA到GPIOH都有一组功能相同的寄存器,当然有些特殊的可能会不一样。这些外设只是基地址不一样,偏移地址都是一样的,这样就可以通过C语言的结构体来对寄存器进行封装。

  C语言结构体的特点是:变量地址是连续的,比如在结构体内定义了2个32个的变量,假设第一个变量的地址为0,那么第二个变量的地址就是4。

  结构体封装寄存器代码如下:

  typedef unsigned int uint32_t; /*无符号 32 位变量*/
  typedef unsigned short int uint16_t; /*无符号 16 位变量*/

  /* GPIO 寄存器列表 */
  typedef struct{
    uint32_t CRL; /*GPIO 端口配置低寄存器 地址偏移: 0x00 */     uint32_t CRH; /*GPIO 端口配置高寄存器 地址偏移: 0x04 */     uint32_t IDR; /*GPIO 数据输入寄存器 地址偏移: 0x08 */     uint32_t ODR; /*GPIO 数据输出寄存器 地址偏移: 0x0C */     uint32_t BSRR; /*GPIO 位设置/清除寄存器 地址偏移: 0x10 */     uint32_t BRR; /*GPIO 端口位清除寄存器 地址偏移: 0x14 */     uint16_t LCKR; /*GPIO 端口配置锁定寄存器 地址偏移: 0x18 */   }GPIO_TypeDef;

  这段代码用typedef关键字声明了名为GPIO_TypeDef的结构体类型,结构体内有7个成员变量,变量名正好是对应寄存器的名字。由于结构体内变量的存储空间是连续的,其中32位的变量占用4个字节,16位的变量占用2个字节,如下图:

 

  也就是说,定义的这个GPIO_TypeDef,假如这个结构体的首地址为0x40010C00(这也是第一个成员变量CRL的地址),那么结构体中第二个成员变量CRH的地址即为0x40010C00+0x04,加上的这个0x04,正是代表CR了所占用的4个字节地址的偏移量。 

  这样的地址偏移与STM32 GPIO外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器,如下面的代码:

  GPIO_TypeDef * GPIOx; //定义一个 GPIO_TypeDef 型结构体指针 GPIOx
  GPIOx = GPIOB_BASE; //把指针地址设置为宏 GPIOH_BASE 地址
  GPIOx->IDR = 0xFFFF;
  GPIOx->ODR = 0xFFFF; 
 
  uint32_t temp;
  temp = GPIOx->IDR; //读取 GPIOB_IDR 寄存器的值到变量 temp 中

  这段代码先用GPIO_TypeDef类型定义一个结构体指针GPIOx,并让指针指向地址GPIOB_BASE(0x40010C00),使其地址确定下来,然后根据C语言访问结构体的语法,用GPIOx->ODR及GPIOx->IDR等方式读写寄存器。

  最后,更进一步,直接使用宏定义好GPIO_TypeDef类型的指针,而且指针指向各个GPIO端口的基地址,使用时直接用该宏访问寄存器即可,代码如下:

  /*使用 GPIO_TypeDef 把地址强制转换成指针*/
  #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   GPIOH   ((GPIO_TypeDef *) GPIOH_BASE)
 
  /*使用定义好的宏直接访问*/
  /*访问 GPIOB 端口的寄存器*/
  GPIOB->BSRR = 0xFFFF; //通过指针访问并修改 GPIOB_BSRR 寄存器
  GPIOB->CRL = 0xFFFF; //修改 GPIOB_CRL 寄存器
  GPIOB->ODR =0xFFFF; //修改 GPIOB_ODR 寄存器

  uint32_t temp;
  temp = GPIOB->IDR; //读取 GPIOB_IDR 寄存器的值到变量 temp 中
 
  /*访问 GPIOA 端口的寄存器*/
  GPIOA->BSRR = 0xFFFF;
  GPIOA->CRL = 0xFFFF;
  GPIOA->ODR =0xFFFF;
 
  uint32_t temp;
  temp = GPIOA->IDR; //读取 GPIOA_IDR 寄存器的值到变量 temp 中

  以此类推,其它外设也同样可以用这种方法来封装。STM32的库也是基于这样的方式封装寄存器的。