【STM32】存储器和位带映射(bit band mapping)

文章目录

    • 0 前言
    • 1 关于地址和存储器
    • 2 STM32内部存储器
    • 3 位带映射(bit band mapping)
    • 4 扩展:IAP

0 前言

  最近在研究stm32标准库,对使用宏定义实现位操作的函数非常感兴趣,简单的一句PAout(1) = 0;就能实现某个引脚电平的输出,非常有51时代的风格,有一种简洁美,于是在仔细阅读参考手册和数据手册的同时结合网上众说纷纭的文章,希望产出一篇正确且全面的文章。

  终于知道为什么谈到单片机一般就是存储器和外设,因为这是对芯片应用者来说最基本也是最重要的东西了。希望这篇文章能够让读者对STM32的存储器有一个全面且略深入的认识。

1 关于地址和存储器

  在开始之前,不妨先回想一下微机原理所学的内容。计算机的中央处理器(CPU)包含三大总线:数据总线,地址总线,控制总线。在和外界交互时,一般是先设置地址总线,选定某一块存储区域,然后将数据放到数据总线上,在控制总线的控制下,读写这块被选定的存储区块,实现CPU和存储器之间的数据交互
  单片机其实同理,所谓单片机,即单片微型计算机,它本质上就是将cpu,存储器和一些外围设备集成到一个芯片上。单片机内核大抵相当于cpu,内部的sram和flash大抵相当于存储器,因此,这些总线(一般在微机中称为内部总线)对开发者来说是不可见的,也无需关心如何连接,这些已经在芯片内部实现了。
  既然集成了存储器和外设,那就需要去读写和控制,怎么操作呢,很简单,就是给他们分配地址,然后读写地址即可,一个地址实际上对应了一个字节,而STM32具有32位地址总线,因此其最大可读写的存储器大小是2^32 Byte = 2^10 * 2^10 * 2^10 * 2^2 = 4 GByte(一个2的10次方等于10进制下的3次方,KB,MB,GB),是不是很惊讶?一个小小的芯片竟然可以控制4GB的内存?!显然,实际上并没有这么多,很大一部分是保留或者给外部扩展用的。

关于外部扩展RAM或者FLASH,一般来说想实现和内部存储器一样访问,需要使用FSMC这个外设,但这个外设只在大容量设备中有,所以对于小容量和中容量的芯片来说,不能扩展外部ram,实现像内部ram一样访问。关于FSMC可以参考这篇文章。

  那外设怎么控制呢?看手册我们会发现,所有外设的控制,都是通过读写寄存器实现的,那寄存器是怎么读写的?实际上也是通过上面提到的地址去访问。在芯片设计时,这些外设都是设定好的,即哪个寄存器的某一位被设置为1,对应外设会有什么反应。然后这些寄存器都分配了一个地址(这也就是为什么不可能4GB全部作为内存来用),开发者只需要去访问这些地址,然后读写该位置的内存即可读写寄存器。但是很显然,这样做非常麻烦,没有人会愿意记一堆地址,因此,芯片厂商一般会提供一个寄存器名称和地址相对应的文件,一般用宏定义来实现。这就是最早的固件库,都是一堆宏定义。早期的单片机编程全部是使用寄存器编程,虽然麻烦,但其实效率更高。

  总结来说,在STM32内部所有的存储器都被分配了一个地址,开发者在使用时需要访问对应地址的内存,这就是芯片开发的本质。

2 STM32内部存储器

  对于玩底层开发的来说,了解芯片的存储结构非常重要,尤其代码里面时不时需要操作寄存器。
  以STM32F103C8T6型号为例,这里截取官方数据手册第34页的Memory mapping:

在这里插入图片描述

从上图可以看出,STM32的存储(4GB)被分成8块,每块512MB(0.5G),其中灰色的部分是保留的,也就是未使用。
  先来看第一部分,也就是标0的那一大块,右侧是具体的分布结构。

在这里插入图片描述
这一部分是存储代码的区域,其中Flash Memory是存放用户编写下载的代码,其起始地址为0x0800 0000,所以在Keil中设置仿真器,起点要设置成这个。
在这里插入图片描述

虽然这里写的size是0x0002 0000,也就是2^17 B = 2^7 kB = 128 kB,但实际STM32F103C8T6数据手册上写的是64kB的Flash,但网上也有人研究如何使用后64k的Flash,有兴趣的可以自行搜索。

System Memory也就是系统主闪存,用于存放芯片的BootLoader程序,下载程序的时候执行。
  但实际上,芯片上电之后,单片机一般是从0地址开始运行的,从图中可以看出,0地址处其实是一个“跳转部分”,根据 BOOT 引脚转向闪存或系统存储器执行程序,也就是经常被讨论的STM32启动配置的问题,上电复位后根据BOOT引脚的电平进入不同的启动模式。

  第2部分,也就是标1的那一大块。这里主要是SRAM所在区域,一般不会直接访问。
  重点是第3部分,这里主要是存储各种外设对应的寄存器。这部分,在参考手册中有更加详细的描述,建议结合标准库代码对照着看。

  在标准库文件stm32f10x.h的1267行,有关于外设存储映射(Peripheral_memory_map)的宏定义:

在这里插入图片描述
这里定义了一些存储器区段基地址的宏,方便开发者使用,比如FLASH_BASE就是上面谈到的FLASH起始地址,就是0x0800 0000。
  值得一提的是,这些宏定义其实就是来自参考手册,包括“总线基地址”。如下图所示,是参考手册中外设寄存器及其对应的地址范围:

在这里插入图片描述

在这里插入图片描述

这是标准库中的代码部分:

在这里插入图片描述

注意区分“外设基地址”,“总线基地址”以及“TIM1(某个外设)基地址”,所谓基地址,其实就是地址范围的起点(一般从低地址到高地址,所以起点是低地址),而参考手册中所谓的偏移地址,所基于的地址就是这个外设地址范围的起点:

在这里插入图片描述

以上图为例,GPIOA_CRH = GPIOA_BASE + OFFSET(0x04),这样就可以得到寄存器的地址。因此,如果需要操作寄存器,可以通过这个地址来访问,但是很显然,这样使用相对(偏移)地址相比于使用绝对地址简单一些,但仍然要记住一大堆地址对应的偏移,非常不方便。
  所以标准库中给出了一种更加简单直观的方式,就是使用结构体,因为这些寄存器地址都是连续的,那么就可以使用一个结构体来依次包含这些寄存器,如下图所示。
在这里插入图片描述
然后再将基地址强制转换成这个结构体的指针:

在这里插入图片描述
这样在使用时,就可以直接以GPIOA->CRL = 0x0100 0030这样的形式,来访问某个寄存器了。
  如果只想改变其中的一位呢?可以使用位运算符,比如|=, &=, ^=,具体用法建议参考这篇文章。

3 位带映射(bit band mapping)

  既然位运算也可以实现位操作,那为什么还需要有位带呢?GPT这样回答:
在这里插入图片描述
emmmm, 听着挺有道理。

  所谓位带,也叫位段(一个是Cortex-M手册中的表达,一个是STM32参考手册中文翻译版中的表达,实际是一个意思,后文统称位带),类比51单片机,那就是位寻址区段,即可以直接位访问的区域。
  先来看看官方参考手册对位带的解释:

在这里插入图片描述

重点是将“别名存储器”区中的每个字映射到位段存储器区的一个位,所谓别名存储器区,实际上就是这块区域的每一个单位的存储器,都具有别名,而不再是单纯的地址,这块存储器区的地址范围也是在上面讨论的4GB地址范围内的(不是另外有一块存储区)。
  这里有一个关键点,那就是“字”,这可不是一个字节。所谓字,实际上取决于cpu的数据总线宽度,也就是所谓的字长,STM32中的32就是指数据总线的宽度(而不是地址总线的宽度,只是一般地址总线会和数据总线位宽保持一致,这样内存访问更加高效)。因此STM32中一个“字”是指4个字节。

一开始我以为映射一个位,一个字节足够,但是STM32“财大气粗”,用4个字节来映射,但我觉得实际取的时候还是取低地址字节,高地址的三个字节更像是用来隔离的

在网上找到一张位带示意图,结构表示得非常清晰明确:

在这里插入图片描述

图片来源

从图中可以看出,在STM32内部存储中,有两个位带区,一个是片上SRAM最低地址1MB范围内,一个是片上外设最低地址1MB,根据图中所示,它所映射到的区域大小是32MB,在高地址处,中间有31MB的空当。

  前面展示的标准库中的各种BASE地址,其实指的都是其位带中的地址,如果我们想实现位操作,还需要得到其对应的别名存储器区对应的地址,根据参考手册,计算公式如下

在这里插入图片描述
一般我们使用的是外设段,SRAM段使用较少。以外设段为例,其别名区的起始地址(bit_band_base)是0x4200 0000,因为是一个位映射到四个字节,所以一个字节映射到32个字节,比例关系是1:32,所以字节偏移量(byte_offset)是乘以32,而位偏移(bit_number)是乘以4。

  基于以上的学习,再来看正点原子提供的sys.h文件中给出的宏定义,就基本可以理解了,这里基于该代码作了一些简单的修改,起名io.h,可以添加到项目中:

io.h

#ifndef __IO_H
#define __IO_H#include "stm32f10x.h"//根据位带存储器区中的地址和位号,得到别名存储器区对应的地址
#define BIT_BAND_ALIAS_ADDR(bit_band_addr, bitnum) (PERIPH_BB_BASE + (bit_band_addr-PERIPH_BASE)<<5 + bitnum<<2)
//使用位运算代替乘法,更高效//获取地址对应的内存
#define MEM_ADDR(addr)  *((volatile unsigned long  *)(addr))
//获取地址和位号对应的别名区映射的内存
#define BIT_ADDR(addr, bitnum)   MEM_ADDR(BIT_BAND_ALIAS_ADDR(addr, bitnum))//IO口地址映射
#define GPIOA_ODR_Addr    (GPIOA_BASE+0x0c) //0x4001080C
#define GPIOB_ODR_Addr    (GPIOB_BASE+0x0c) //0x40010C0C
#define GPIOC_ODR_Addr    (GPIOC_BASE+0x0c) //0x4001100C
#define GPIOD_ODR_Addr    (GPIOD_BASE+0x0c) //0x4001140C
#define GPIOE_ODR_Addr    (GPIOE_BASE+0x0c) //0x4001180C
#define GPIOF_ODR_Addr    (GPIOF_BASE+0x0c) //0x40011A0C
#define GPIOG_ODR_Addr    (GPIOG_BASE+0x0c) //0x40011E0C#define GPIOA_IDR_Addr    (GPIOA_BASE+0x08) //0x40010808
#define GPIOB_IDR_Addr    (GPIOB_BASE+0x08) //0x40010C08
#define GPIOC_IDR_Addr    (GPIOC_BASE+0x08) //0x40011008
#define GPIOD_IDR_Addr    (GPIOD_BASE+0x08) //0x40011408
#define GPIOE_IDR_Addr    (GPIOE_BASE+0x08) //0x40011808
#define GPIOF_IDR_Addr    (GPIOF_BASE+0x08) //0x40011A08
#define GPIOG_IDR_Addr    (GPIOG_BASE+0x08) //0x40011E08//IO口操作,只对单一的IO口!
//确保n的值小于16!
#define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n)  //输出
#define PAin(n)    BIT_ADDR(GPIOA_IDR_Addr,n)  //输入#define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n)  //输出
#define PBin(n)    BIT_ADDR(GPIOB_IDR_Addr,n)  //输入#define PCout(n)   BIT_ADDR(GPIOC_ODR_Addr,n)  //输出
#define PCin(n)    BIT_ADDR(GPIOC_IDR_Addr,n)  //输入#define PDout(n)   BIT_ADDR(GPIOD_ODR_Addr,n)  //输出
#define PDin(n)    BIT_ADDR(GPIOD_IDR_Addr,n)  //输入#define PEout(n)   BIT_ADDR(GPIOE_ODR_Addr,n)  //输出
#define PEin(n)    BIT_ADDR(GPIOE_IDR_Addr,n)  //输入#define PFout(n)   BIT_ADDR(GPIOF_ODR_Addr,n)  //输出
#define PFin(n)    BIT_ADDR(GPIOF_IDR_Addr,n)  //输入#define PGout(n)   BIT_ADDR(GPIOG_ODR_Addr,n)  //输出
#define PGin(n)    BIT_ADDR(GPIOG_IDR_Addr,n)  //输入#endif

另外,从上图可以看出,高地址的存储区域,主要存放的就是内核相关的东西,比如NVIC,TPIU等。这也就能理解为什么stm32标准库中内核相关的代码,比如中断,要放在misc文件中,和其他外设文件形成鲜明对比,可能就是因为本身地址差别比较大。

4 扩展:IAP

  IAP的原理与上面两种有较大区别,这种方式将主存储区又分成了两个区域(根据实际需要由开发者自行分配),0800 0000起始处的这部分,存储一个开发者自己设计的Bootloader程序,另一部分存储真正需要运行的APP程序。

单片机的Bootloader程序,其主要作用就是给单片机升级。在单片机启动时,首先从Bootloader程序启动,一般情况不需要升级,就会立即从Bootloader程序跳转到存储区另一部分的APP程序开始运行。

假如Bootloader程序时,需要进行升级(比如APP程序运行时,接收到升级指令,可以在flash中的特定位置设置一个标志,然后触发重启,重启后进入Bootloader程序,Bootloader程序根据标志位就能判断是否需要升级),则会通过某种方式(比如通过WIFI接收升级包,或借助另一块单片机接收升级包,Bootloader再通过串口或SPI等方式从另一块单片机获取升级包数据)先将接收到的程序写入存储区中存储APP程序的那个位置,写入完成后再跳转到该位置,即实现了程序的升级

在这里插入图片描述

参考链接

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/596690.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTTP协议报文的结构的补充和from表单以及ajax表单

响应 状态码 表示了这次请求对应的响应,是什么样的状态(成功,失败,还是其他的情况.还有及其对应的原因&#xff09; 主要有这些类 成功状态码&#xff1a;其中200最常见&#xff0c;表示成功 重定向状态码&#xff1a;很多时候,页面跳转,就可以通过重定向来实现. 还有的时…

jdk api之AbstractMethodError基础、应用、实战

博主18年的互联网软件开发经验&#xff0c;从一名程序员小白逐步成为了一名架构师&#xff0c;我想通过平台将经验分享给大家&#xff0c;因此博主每天会在各个大牛网站点赞量超高的博客等寻找该技术栈的资料结合自己的经验&#xff0c;晚上进行用心精简、整理、总结、定稿&…

蓝桥杯算法题:卡片换位

问题描述 你玩过华容道的游戏吗&#xff1f;这是个类似的&#xff0c;但更简单的游戏。 看下面 2 x 3 的格子 --------- | A | * | * | --------- | B | | * | --------- 1 2 3 4 5 在其中放 5 张牌&#xff0c;其中 A 代表关羽&#xff0c;B 代表张飞&#xff0c;* 代表士兵…

SSL协议是什么?有什么作用?

SSL协议是一种让互联网上的数据传输变得更安全的技术。它的主要作用是&#xff1a; 保密性&#xff1a; 使用加密手段&#xff0c;让别人偷看不了你在网上发的信息&#xff08;比如密码、聊天内容、银行卡号等&#xff09;。完整性&#xff1a;防止你的信息在传输途中被偷偷修…

隐私计算实训营学习八:隐语SCQL的开发实践

文章目录 一、SCQL使用集成最佳实践1.1 SCQL使用流程1.2 SCQL部署1.3 SCQL使用示例 二、SCQL工作原理三、使用SecretNote上手体验SCQL 一、SCQL使用集成最佳实践 1.1 SCQL使用流程 SCQL使用&#xff1a; SCQL 开放 API 供⽤户使⽤/集成。可以使⽤SCDBClient上⼿体验(类似与My…

51单片机ESP8266WiFi模块简介

乐鑫与安信可关系 乐鑫是生产esp8266芯片的厂家&#xff0c;安信可是基于esp8266芯片生产模组的厂家&#xff0c;所谓模组是基于芯片制作的套件。 波特率 ESP8266系列模组出厂使用的是AT固件&#xff0c;默认波特率是115200。实际上&#xff0c;模组在上电过程中首先是在748…

4月6号排序算法(2)

堆排序 讲堆排序之前我们需要了解几个定义 什么叫做最大堆&#xff0c;父亲节点&#xff0c;以及孩子节点 将根节点最大的堆叫做最大堆或大根堆&#xff0c;根节点最小的堆叫做最小堆或小根堆。 每个节点都是它的子树的根节点的父亲 。 反过来每个节点都是它父亲的孩子 。 …

二维相位解包理论算法和软件【全文翻译- 质量分布图(3.3)】

在本节中,我们将定义几个在相位解包中非常有用的质量映射。质量图是定义给定相位数据中每个像素质量或好坏的数值数组。它们对于指导第 4 章将要介绍的几种路径跟踪算法是必要的,对于第 5 章将要介绍的一些加权 L^P-norm 算法也是必要的。 我们要讨论的第一个质量图是相关图,…

UART设计

一、UART通信简介 通用异步收发器&#xff0c; 特点&#xff1a;串行、异步、全双工通信 优点&#xff1a;通信线路简单&#xff0c;传输距离远 缺点&#xff1a;传输速度慢 数据传输速率&#xff1a;波特率&#xff08;单位&#xff1a;baud&#xff0c;波特&#xff09; …

4、双指针-移动零

首先不能复制&#xff0c;只能在原数组是哪个操作&#xff0c;那么很多集合的方式就不行了。当然在现实开发中肯定是可以的。目前按照题目来说是不可以的。所以我们可以思考下&#xff0c;是否可以通过交换来实现。 初始化一个变量 to 为 0。这个变量的目的是跟踪非零元素应该…

云计算存在的安全隐患

目录 一、概述 二、ENISA云安全漏洞分析 三、云计算相关系统漏洞 3.1 概述 3.2 漏洞分析 3.2.1 Hypervisor漏洞 3.2.1.1 CVE-2018-16882 3.2.1.2 CVE-2017-17563 3.2.1.3 CVE-2010-1225 3.2.2 虚拟机漏洞 3.2.2.1 CVE-2019-14835 3.2.2.2 CVE-2019-5514 3.2.2.3 CV…

观测线程的工具——jconsole

joconsole的简单使用 joncole位置在jdk/bin路径中&#xff0c;在进入路径后可以查找到jconsole.exe的应用程序。如图&#xff1a; 双击创建jconsole进程&#xff0c;可以在里面选择所要观测的java文件。 以我的代码为例&#xff1a; class MyThread extends Thread {Overrid…