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

news2024/11/20 0:40:32

文章目录

    • 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.coloradmin.cn/o/1572971.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JavaWeb】Day35.MySQL概述——数据库设计-DDL(二)

表操作 关于表结构的操作也是包含四个部分&#xff1a;创建表、查询表、修改表、删除表。 1.创建 语法 create table 表名( 字段1 字段1类型 [约束] [comment 字段1注释 ], 字段2 字段2类型 [约束] [comment 字段2注释 ], ...... 字段n 字段n类型 [约束] [comment …

phpstorm设置头部注释和自定义注释内容

先说设置位置&#xff1a; PhpStorm中文件、类、函数等注释的设置在&#xff1a;setting-》Editor-》FIle and Code Template-》Includes-》PHP Function Doc Comment下设置即可&#xff0c;其中方法的默认是这样的&#xff1a; /** ${PARAM_DOC} #if (${TYPE_HINT} ! "…

SpringBoot新增员工模块开发

需求分析与设计 一&#xff1a;产品原型 一般在做需求分析时&#xff0c;往往都是对照着产品原型进行分析&#xff0c;因为产品原型比较直观&#xff0c;便于我们理解业务。 后台系统中可以管理员工信息&#xff0c;通过新增员工来添加后台系统用户。 新增员工原型&#xf…

4.1 JavaScript的使用

JavaScript有两种使用方式&#xff1a;一是在HTML文档中直接添加代码&#xff1b;二是将JavaScript脚本代码写到外部的JavaScript文件中&#xff0c;再在HTML文档中引用该文件的路径地址。 这两种使用方式的效果完全相同&#xff0c;可以根据使用率和代码量选择相应的开发方式。…

【ControlNet v3版本论文阅读】

网络部分最好有LDM或者Stable Diffusion的基础&#xff0c;有基础的话会看的很轻松 Abstract 1.提出了一种网络结构支持额外输入条件控制大型预训练的扩散模型。利用预训练模型学习一组不同的条件控制。 2.ControlNet对于小型&#xff08;<50k&#xff09;或大型&#xff…

经典机器学习模型(九)EM算法在高斯混合模型中的应用

经典机器学习模型(九)EM算法在高斯混合模型中的应用 EM算法的推导可以参考&#xff1a; 经典机器学习模型(九)EM算法的推导 若随机变量X服从一个数学期望为 μ μ μ、方差为 σ 2 σ^2 σ2的正态分布&#xff0c;可以记为 N ( μ &#xff0c; σ 2 ) N(μ&#xff0c;σ2)…

二叉树进阶——手撕二叉搜索树

troop主页&#xff1a;troop 手撕二叉搜索树 1.二叉搜索树的定义2.实现&#xff08;非递归&#xff09;补充结构2.1查找2.2插入2.3删除&#xff08;重要&#xff09;情况1(无孩子&&一个孩子&#xff09; 3.二叉搜索树的应用3.1K模型3.2KV模型3.2.1KV模型的实现 总结二叉…

「每日跟读」英语常用句型公式 第4篇

「每日跟读」英语常用句型公式 第4篇 1. I’ve decided to ____ 我决定要____了 I’ve decided to take a vacation (我决定要去度假) I’ve decided to change my lifestyle (我决定要改变我的生活方式) I’ve decided to adopt a dog (我决定要收养一条狗了) I’ve dec…

【AOSP】手把手教你编译和调试AOSP源码

一、下载AOSP源码 在开始之前&#xff0c;我们先安装编译AOSP需要的一些系统基本依赖&#xff0c;如下命令 sudo apt-get install git-core gnupg flex bison gperf build-essential zip curl zlib1g-dev gcc-multilib g-multilib libc6-dev-i386 lib32ncurses5-dev x11proto…

三子棋游戏----C语言版【超级详细 + 视频演示 + 完整源码】

㊙️小明博客主页&#xff1a;➡️ 敲键盘的小明 ㊙️ ✅关注小明了解更多知识☝️ 文章目录 前言一、三子棋的实现思路二、三子棋的实现步骤2.1 先显示游戏的菜单2.2 游戏的具体实现2.2.1 棋盘的初始化2.2.2 展示棋盘2.2.3 下棋&#x1f534;玩家下棋&#x1f534;电脑下棋2.2…

OpenHarmony开发-系统烧录

本文详细介绍了烧录OpenHarmony系统到开发板的操作流程。从基础的硬件准备和软件环境设置入手&#xff0c;详细说明了如何配置开发环境、构建系统镜像等过程&#xff0c;详细描述了烧录过程中的关键步骤&#xff0c;以及如何使用专用工具将OpenHarmony系统镜像传输到开发板。同…

【Rust】环境搭建

Rust 支持很多的集成开发环境&#xff08;IDE&#xff09;或开发专用的文本编辑器。 官方网站公布支持的工具如下&#xff08;工具 - Rust 程序设计语言&#xff09; 本课程将使用 Visual Studio Code 作为我们的开发环境&#xff08;Eclipse 有专用于 Rust 开发的版本&#…

vue使用iview导航栏Menu activeName不生效

activeName不生效 一、问题一、解决方案&#xff0c; 一、问题 根据ivew官网的提示&#xff0c;设置了active-name和open-names以后&#xff0c;发现不管是设置静态是数据还是设置动态的数据&#xff0c;都不生效 一、解决方案&#xff0c; 在设置动态名称的时候&#xff0c…

docker笔记(一):安装、常用命令

一、docker概述 1.1docker为什么会出现 各种环境配置十分繁琐&#xff0c;每一个机器都需要配置环境&#xff0c;难免出现各种问题。 发布一个项目jar需要配置&#xff08;MySQL、redis、jdk、…&#xff09;&#xff0c;项目不能都带上环境安装打包&#xff1a; 传统&…

Redis实战篇-集群环境下的并发问题

实战篇Redis 3.7 集群环境下的并发问题 通过加锁可以解决在单机情况下的一人一单安全问题&#xff0c;但是在集群模式下就不行了。 1、我们将服务启动两份&#xff0c;端口分别为8081和8082&#xff1a; 2、然后修改nginx的conf目录下的nginx.conf文件&#xff0c;配置反向代…

前端:注册页面(后端php实现)

效果 代码 Regist.php <!-- 内部员工注册 --> <?php require_once get_db_conn.php; $conn db_connect();?> <?php //设置变量的默认值 if (!isset($_POST[UserID])) {$_POST[UserID] ; } if (!isset($_POST[Password])) {$_POST[Password] ; } if (!i…

数据字典

文章目录 一、需求分析二、表设计&#xff08;两张表&#xff09;三、功能实现3.1 数据字典功能3.1.1 列表功能3.1.2 新增数据字典3.1.3 编辑数据字典 3.2 数据字典明细3.2.1 列表功能3.2.2 新增字典明细3.2.3 编辑字典明细 3.3 客户管理功能3.3.1 列表功能3.3.2 新增用户3.3.3…

【最新可用】Claude国内镜像,可上传图片,可用Claude3全系模型(包括Pro版本的Opus)!亲测比GPT好用!

亲测可用&#xff0c;镜像地址&#xff1a;Claude 3 镜像 使用方法 访问镜像&#xff1a;Claude 3 镜像 2. 点击设置&#xff0c;配置授权码&#xff0c;关闭设置。这里免费赠送一个体验版的授权码 sk-SZcJyvx3RXRID624E2D3795578Df44C7Af03F2909a8f5eA0 即可发起对话啦&…

总包不足80w的高龄Android程序员,被面试官diss混得太差,网友狂吐槽……

有网友直言&#xff1a;90%的人一辈子一年也拿不到80万 有网友分析到&#xff1a;看面试情况&#xff0c;没什么希望就直接其实我觉得30岁年薪低于1000万的都是loser&#xff0c;你我都是 有网友说&#xff1a;这几年互联网行业极大发展&#xff0c;让互联网行业成为了明星行…

Python-VBA编程500例-033(入门级)

角色定位(Role Positioning)在编程中的实际应用场景主要体现在以下几个方面&#xff1a; 1、权限管理&#xff1a;在开发企业级应用或复杂的系统时&#xff0c;角色定位用于定义和管理用户的权限。例如&#xff0c;一个系统可能有管理员、普通用户、访客等不同角色&#xff0c…