第四章 什么是寄存器

描述

单芯片解决方案,开启全新体验——W55MH32 高性能以太网单片机

W55MH32是WIZnet重磅推出的高性能以太网单片机,它为用户带来前所未有的集成化体验。这颗芯片将强大的组件集于一身,具体来说,一颗W55MH32内置高性能Arm® Cortex-M3核心,其主频最高可达216MHz;配备1024KB FLASH与96KB SRAM,满足存储与数据处理需求;集成TOE引擎,包含WIZnet全硬件TCP/IP协议栈、内置MAC以及PHY,拥有独立的32KB以太网收发缓存,可供8个独立硬件socket使用。如此配置,真正实现了All-in-One解决方案,为开发者提供极大便利。 

在封装规格上,W55MH32 提供了两种选择:QFN100和QFN68。

W55MH32L采用QFN100封装版本,尺寸为12x12mm,其资源丰富,专为各种复杂工控场景设计。它拥有66个GPIO、3个ADC、12通道DMA、17个定时器、2个I2C、5个串口、2个SPI接口(其中1个带I2S接口复用)、1个CAN、1个USB2.0以及1个SDIO接口。如此丰富的外设资源,能够轻松应对工业控制中多样化的连接需求,无论是与各类传感器、执行器的通信,还是对复杂工业协议的支持,都能游刃有余,成为复杂工控领域的理想选择。 同系列还有QFN68封装的W55MH32Q版本,该版本体积更小,仅为8x8mm,成本低,适合集成度高的网关模组等场景,软件使用方法一致。更多信息和资料请进入http://www.w5500.com/网站或者私信获取。 

此外,本W55MH32支持硬件加密算法单元,WIZnet还推出TOE+SSL应用,涵盖TCP SSL、HTTP SSL以及 MQTT SSL等,为网络通信安全再添保障。 

为助力开发者快速上手与深入开发,基于W55MH32L这颗芯片,WIZnet精心打造了配套开发板。开发板集成WIZ-Link芯片,借助一根USB C口数据线,就能轻松实现调试、下载以及串口打印日志等功能。开发板将所有外设全部引出,拓展功能也大幅提升,便于开发者全面评估芯片性能。 

若您想获取芯片和开发板的更多详细信息,包括产品特性、技术参数以及价格等,欢迎访问官方网页:http://www.w5500.com/,我们期待与您共同探索W55MH32的无限可能。

单片机

第四章 什么是寄存器

本章参考资料:《W55MH32_数据手册_V1.0.0》、《W55MH 32参考手册_V1.0.0》

学习本章时,配合《W55MH 32参考手册_V1.0.0》“存储器和总线架构”及“通用I/O(GPIO)”章节一起阅读,效果会更佳,特别是涉及到寄存器说明的部分。

1 什么是寄存器

我们经常说寄存器,那么什么是寄存器?这是我们本章需要讲解的内容,在学习的过程中,大家带着这个疑问好好思考下,到最后看看大家能否用一句话给寄存器下一个定义。

2 W55MH32

我们开发板中使用的芯片是100pin的W55MH32L,具体见下图。这个就是我们接下来要学习的高性能以太网单片机,它将带领我们进入嵌入式的殿堂。

芯片四周是引脚,开发板中把芯片的引脚引出来,连接到各种传感器上,然后在W55MH32L上编程(实际就是通过程序控制这些引脚输出高电平或者低电平)来控制各种传感器工作, 通过做实验的方式来学习W55MH32芯片的各个资源。开发板是一种评估板,板载资源非常丰富,引脚复用比较多, 力求在一个板子上验证芯片的全部功能。

单片机单片机

3 芯片里面有什么

我们看到的芯片是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内核与外设就如同电脑上的CPU与主板、内存、显卡、硬盘的关系。

W55MH32采用的是Cortex-M3内核,内核即CPU,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。 如GPIO、USART(串口)、I2C、SPI等都叫做片上外设。 

单片机

芯片(这里指内核,或者叫CPU)和外设之间通过各种总线连接,其中驱动单元有4个,被动单元也有4个, 具体见图 W55MH32系统框图 。为了方便理解,我们都可以把驱动单元理解成是CPU部分,被动单元都理解成外设。 下面我们简单介绍下驱动单元和被动单元的各个部件。

3.1 ICode总线

ICode中的I表示Instruction,即指令。我们写好的程序编译之后都是一条条指令,存放在FLASH中, 内核要读取这些指令来执行程序就必须通过ICode总线,它几乎每时每刻都需要被使用,它是专门用来取指的。

3.2 驱动单元

3.2.1 DCode总线

DCode中的D表示Data,即数据,那说明这条总线是用来取数的。我们在写程序的时候,数据有常量和变量两种, 常量就是固定不变的,用C语言中的const关键字修饰,是放到内部的FLASH当中的,变量是可变的,不管是全局变量还是局部变量都放在内部的SRAM。 因为数据可以被Dcode总线和DMA总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。

3.2.2 系统总线

系统总线主要是访问外设的寄存器,我们通常说的寄存器编程,即读写寄存器都是通过这根系统总线来完成的。

3.2.3 DMA总线

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

3.3 被动单元

3.3.1 内部的闪存存储器

内部的闪存存储器即FLASH,我们编写好的程序就放在这个地方。内核通过ICode总线来取里面的指令。

3.3.2 内部的SRAM

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

3.3.3 FSMC

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

3.3.4 AHB到APB的桥

从AHB总线延伸出来的两条APB2和APB1总线,上面挂载着W55MH32各种各样的特色外设。我们经常说的GPIO、串口、I2C、SPI这些外设就挂载在这两条总线上, 这个是我们学习W55MH32的重点,就是要学会编程这些外设去驱动外部的各种设备。

单片机

4 存储器映射

在W55MH32系统框图中,程序存储器、数据存储器、寄存器和输入输出端口被组织在同一个 4GB 的线性地址空间内。数据字节以小端格式存放在存储器中。一个字里的最低地址字节被认为是该字的最低有效字节,而最高地址字节是最高有效字节。可访问的存储器空间被分成 8 个主要块,每个块为 512MB。我们在编程的时候,可以通过他们的地址找到他们,然后来操作他们(通过C语言对它们进行数据的读和写)。

4.1 存储器映射

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

单片机

4.1.1 存储器区域功能划分

在这4GB的地址空间中,ARM已经粗线条的平均分成了8个块,每块512MB,每个块也都规定了用途,具体分类见表格 存储器功能分类 。 每个块的大小都有512MB,显然这是非常大的,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分而已。

序号 用途 地址范围
Block 0 Code 0x0000 0000 ~ 0x1FFF FFFF(512MB)
Block 1 SRAM 0x2000 0000 ~ 0x3FFF FFFF(512MB)
Block 2 片上外设 0x4000 0000 ~ 0x5FFF FFFF(512MB)
Block 3 FSMC 的 bank1 ~ bank2 0x6000 0000 ~ 0x7FFF FFFF(512MB)
Block 4 FSMC 的 bank3 ~ bank4 0x8000 0000 ~ 0x9FFF FFFF(512MB)
Block 5 FSMC 寄存器 0xA000 0000 ~ 0xCFFF FFFF(512MB)
Block 6 没有使用 0xD000 0000 ~ 0xDFFF FFFF(512MB)
Block 7 Cortex-M3 内部外设 0xE000 0000 ~ 0xFFFF FFFF(512MB)

在这8个Block里面,有3个块非常重要,也是我们最关心的三个块。Block0用来设计成内部FLASH,Block1用来设计成内部RAM, Block2用来设计成片上的外设,下面我们简单的介绍下这三个Block里面的具体区域的功能划分。

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

Block0主要用于设计片内的FLASH,要在芯片内部集成更大的FLASH或者SRAM都意味着芯片成本的增加,往往片内集成的FLASH都不会太大, W55MH32能在追求性价比的同时做到512KB,实乃良心之举。Block内部区域的功能划分具体见表格 存储器Block0内部区域功能划分 。

用途说明 地址范围
Block0 预留 0x1FFE C008 ~ 0x1FFF FFFF
选项字节:用于配置读写保护、BOR 级别、软件 / 硬件看门狗以及器件处于待机或停止模式下的复位。当芯片被锁住后,可从 RAM 启动修改对应寄存器位。 0x1FFF F800 - 0x1FFF F80F
系统存储器:存放 ST 出厂烧写的 ISP 自举程序(Bootloader),用户无法改动,串口下载需用此程序。 0x1FFF F000 - 0x1FFF F7FF
预留 0x0808 0000 ~ 0x1FFF EFFF
FLASH:程序存放位置 0x0800 0000 ~ 0x0807 FFFF (512KB)
预留 0x0008 0000 ~ 0x07FF FFFF
取决于 BOOT 引脚,为 FLASH、系统存储器、SRAM 的别名。 0x0000 0000 ~ 0x0007 FFFF

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

Block1用于设计片内的SRAM,Block内部区域的功能划分具体见表格 存储器Block1内部区域功能划分 。

用途说明 地址范围
Block1 预留 0x2001 0000 ~ 0x3FFF FFFF
SRAM 64KB 0x2000 0000 ~ 0x2000 FFFF

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

Block2用于设计片内的外设,根据外设的总线速度不同,Block被分成了APB和AHB两部分,其中APB又被分为APB1和APB2, 具体见表格 存储器Block2内部区域功能划分 。

用途说明 地址范围
Block2 APB1 总线外设 0x4000 0000 ~ 0x4000 77FF
APB2 总线外设 0x4001 0000 ~ 0x4001 3FFF
AHB 总线外设 0x4001 8000 ~ 0x5003 FFFF

5 寄存器映射

我们知道,存储器本身没有地址,给存储器分配地址的过程叫存储器映射,那什么叫寄存器映射?寄存器到底是什么?

在存储器Block2这块区域,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能, 当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元, 如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名, 这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。

比如,我们找到GPIOB端口的输出数据寄存器ODR的地址是0x40010C0C(至于这个地址如何找到可以先跳过,后面我们会有详细的讲解), ODR寄存器是32bit,低16bit有效,对应着16个外部IO,写0/1对应的的IO则输出低/高电平。现在我们通过C语言指针的操作方式, 让GPIOB的16个IO都输出高电平,具体见代码清单:寄存器-1。

代码清单:寄存器-1 通过绝对地址访问内存单元

 

// GPIOB 端口全部输出 高电平
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;

 

0x4001 0C0C在我们看来是GPIOB端口ODR的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数, 要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针, 即(unsigned int *)0x4001 0C0C,然后再对这个指针进行 * 操作。

刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器的方式来操作,具体见代码清单:寄存器-2。

代码清单:寄存器-2 通过寄存器别名方式访问内存单元

 

// GPIOB 端口全部输出 高电平
#define GPIOB_ODR (unsigned int*)(GPIOB_BASE+0x0C)
* GPIOB_ODR = 0xFF;

 

为了方便操作,我们干脆把指针操作“*”也定义到寄存器别名里面,具体见代码清单:寄存器-3。

代码清单:寄存器-3 通过寄存器别名访问内存单元

 

// GPIOB 端口全部输出 高电平
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
GPIOB_ODR = 0xFF;

 

5.1 W55MH32的外设地址映射

片上外设区分为三条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB1挂载低速外设,APB2和AHB挂载高速外设。 相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中APB1总线的地址最低,片上外设从这里开始,也叫外设基地址。

5.1.1 总线基地址

总线名称 总线基地址 相对外设基地址的偏移
APB1 0x4000 0000 0x0
APB2 0x4001 0000 0x0001 0000
AHB 0x4001 8000 0x0001 8000

表格 总线基地址 的“相对外设基地址偏移”即该总线地址与“片上外设”基地址0x4000 0000的差值。关于地址的偏移我们后面还会讲到。

5.1.2 外设基地址

在XX外设的地址范围内,分布着的就是该外设的寄存器。以GPIO外设为例,GPIO是通用输入输出端口的简称, 简单来说就是W55MH32可控制的引脚,基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把GPIO的引脚连接到LED灯的阴极, LED灯的阳极接电源,然后通过W55MH32控制该引脚的电平,从而实现控制LED灯的亮灭。

GPIO有很多个寄存器,每一个都有特定的功能。每个寄存器为32bit,占四个字节,在该外设的基地址上按照顺序排列, 寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以GPIOB端口为例,来说明GPIO都有哪些寄存器, 具体见表格 GPIOB端口的寄存器地址列表:

寄存器名称 寄存器地址 相对 GPIOB 基址的偏移
GPIOB_CRL 0x4001 0C00 0x00
GPIOB_CRH 0x4001 0C04 0x04
GPIOB_IDR 0x4001 0C08 0x08
GPIOB_ODR 0x4001 0C0C 0x0C
GPIOH_BSRR 0x4001 0C10 0x10
GPIOH_BRR 0x4001 0C14 0x14
GPIOH_LOCKR 0x4001 0C18 0x18

有关外设的寄存器说明可参考《W55MH32参考手册》中具体章节的寄存器描述部分,在编程的时候我们需要反复的查阅外设的寄存器说明。

这里我们以“GPIO端口置位/复位寄存器”为例,教大家如何理解寄存器的说明, 具体见图 GPIO端口置位_复位寄存器说明 。

单片机

1 名称

寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…E)”这段的意思是该寄存器名为“GPIOx_BSRR”其中的“x”可以为A-E, 也就是说这个寄存器说明适用于GPIOA、GPIOB至GPIOE,这些GPIO端口都有这样的一个寄存器。

2 偏移地址

偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是0x10, 从参考手册中我们可以查到GPIOA外设的基地址为0x4001 0800 , 我们就可以算出GPIOA的这个GPIOA_BSRR寄存器的地址为:0x4001 0800+0x10;同理, 由于GPIOB的外设基地址为0x4001 0C00, 可算出GPIOB_BSRR寄存器的地址为:0x4001 0C00+0x10 。其他GPIO端口以此类推即可。

3 寄存器位表

紧接着的是本寄存器的位表,表中列出它的0-31位的名称及权限。表上方的数字为位编号,中间为位名称,最下方为读写权限,其中w表示只写, r表示只读,rw表示可读写。本寄存器中的位权限都是w,所以只能写,如果读本寄存器,是无法保证读取到它真正内容的。而有的寄存器位只读, 一般是用于表示W55MH32外设的某种工作状态的,由W55MH32硬件自动更改,程序通过读取那些寄存器位来判断外设的工作状态。

4 位功能说明

位功能是寄存器说明中最重要的部分,它详细介绍了寄存器每一个位的功能。例如本寄存器中有两种寄存器位,分别为BRy及BSy, 其中的y数值可以是0-15,这里的0-15表示端口的引脚号,如BR0、BS0用于控制GPIOx的第0个引脚,若x表示GPIOA,那就是控制GPIOA的第0引脚, 而BR1、BS1就是控制GPIOA第1个引脚。

其中BRy引脚的说明是“0:不会对相应的ODRx位执行任何操作;1:对相应ODRx位进行复位”。这里的“复位”是将该位设置为0的意思, 而“置位”表示将该位设置为1;说明中的ODRx是另一个寄存器的寄存器位,我们只需要知道ODRx位为1的时候,对应的引脚x输出高电平, 为0的时候对应的引脚输出低电平即可(感兴趣的读者可以查询该寄存器GPIOx_ODR的说明了解)。所以,如果对BR0写入“1”的话, 那么GPIOx的第0个引脚就会输出“低电平”,但是对BR0写入“0”的话,却不会影响ODR0位,所以引脚电平不会改变。要想该引脚输出“高电平”, 就需要对“BS0”位写入“1”,寄存器位BSy与BRy是相反的操作。

5.2 C语言对寄存器的封装

以上所有的关于存储器映射的内容,最终都是为大家更好地理解如何用C语言控制读写外设寄存器做准备,此处是本章的重点内容。

5.2.1 封装总线和外设基地址

在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以他们的名字作为宏名, 具体见代码清单:寄存器-4 。

代码清单:寄存器-4 总线和外设基址宏定义

 

/* 外设基地址 */
#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)

 

代码清单:寄存器-4 首先定义了 “片上外设”基地址PERIPH_BASE,接着在PERIPH_BASE上加入各个总线的地址偏移, 得到APB1、APB2总线的地址APB1PERIPH_BASE、APB2PERIPH_BASE,在其之上加入外设地址的偏移,得到GPIOA-G的外设地址, 最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具体地址,就可以用指针读写, 具体见代码清单:寄存器-5 。

代码清单:寄存器-5 使用指针控制BSRR寄存器

 

/* 控制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宏的数值强制转换成了地址,然后再用“*”号做取指针操作,对该地址的赋值, 从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取W55MH32外设的状态。

5.2.2 封装寄存器列表

用上面的方法去定义地址,还是稍显繁琐,例如GPIOA-GPIOE都各有一组功能相同的寄存器,如GPIOA_ODR/GPIOB_ODR/GPIOC_ODR等等, 它们只是地址不一样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入C语言中的结构体语法对寄存器进行封装, 具体见 代码清单:寄存器-6 。

代码清单:寄存器-6 使用结构体对GPIO寄存器组的封装

 

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个 成员变量,变量名正好对应寄存器的名字。 C语言的语法规定,结构体内变量的存储空间是连续的,其中32位的变量占用4个字节,16位的变量占用2个字节, 具体见图 GPIO_TypeDef结构体成员的地址偏移 。

单片机

也就是说,我们定义的这个GPIO_TypeDef ,假如这个结构体的首地址为0x4001 0C00(这也是第一个成员变量CRL的地址), 那么结构体中第二个成员变量CRH的地址即为0x4001 0C00 +0x04 ,加上的这个0x04,正是代表CRL所占用的4个字节地址的偏移量, 其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给。

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

代码清单:寄存器-7 通过结构体指针访问寄存器

 

GPIO_TypeDef * GPIOx; //定义一个GPIO_TypeDef型结构体指针GPIOx
GPIOx = GPIOB_BASE; //把指针地址设置为宏GPIOB_BASE地址
GPIOx- >IDR = 0xFFFF;
GPIOx- >ODR = 0xFFFF;

uint32_t temp;
temp = GPIOx- >IDR; //读取GPIOB_IDR寄存器的值到变量temp中

 

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

最后,我们更进一步,直接使用宏定义好GPIO_TypeDef类型的指针,而且指针指向各个GPIO端口的首地址, 使用时我们直接用该宏访问寄存器即可,具体 代码清单:寄存器-8 。

代码清单:寄存器-8 定义好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中

 

这里我们仅是以GPIO这个外设为例,给大家讲解了C语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。好消息是, 这部分工作都由固件库帮我们完成了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然。

5.3 修改寄存器的位操作方法

使用C语言对寄存器赋值时,我们常常要求只修改该寄存器的某几位的值,且其它的寄存器位不变,这个时候我们就需要用到C语言的位操作方法了。

5.3.1 把变量的某位清零

此处我们以变量a代表寄存器,并假设寄存器中本来已有数值,此时我们需要把变量a的某一位清零,且其它位不变, 方法见 代码清单:寄存器-9 。

代码清单:寄存器-9 对某位清零

 

//定义一个变量a = 1001 1111 b (二进制数)
unsigned char a = 0x9f;

//对bit2 清零

a &= ~(1< <2);

//括号中的1左移两位,(1< <2)得二进制数:0000 0100 b
//按位取反,~(1< <2)得1111 1011 b
//假如a中原来的值为二进制数: a = 1001 1111 b
//所得的数与a作”位与&”运算,a = (1001 1111 b)&(1111 1011 b),
//经过运算后,a的值 a=1001 1011 b
// a的bit2 位被清零,而其它位不变。

 

5.3.2 把变量的某几个连续位清零

由于寄存器中有时会有连续几个寄存器位用于控制某个功能,现假设我们需要把寄存器的某几个连续位清零, 且其它位不变,方法见 代码清单:寄存器-10 。

代码清单:寄存器-10 对某几个连续位清零

 

//若把a中的二进制位分成2个一组
//即bit0、bit1为第0组,bit2、bit3为第1组,
// bit4、bit5为第2组,bit6、bit7为第3组
//要对第1组的bit2、bit3清零

a &= ~(3< <2*1);

//括号中的3左移两位,(3< <2*1)得二进制数:0000 1100 b
//按位取反,~(3< <2*1)得1111 0011 b
//假如a中原来的值为二进制数: a = 1001 1111 b
//所得的数与a作”位与&”运算,a = (1001 1111 b)&(1111 0011 b),
//经过运算后,a的值 a=1001 0011 b
// a的第1组的bit2、bit3被清零,而其它位不变。

//上述(~(3< <2*1))中的(1)即为组编号;如清零第3组bit6、bit7此处应为3
//括号中的(2)为每组的位数,每组有2个二进制位;若分成4个一组,此处即为4
//括号中的(3)是组内所有位都为1时的值;若分成4个一组,此处即为二进制数“1111 b”

//例如对第2组bit4、bit5清零
a &= ~(3< <2*2);

 

5.3.3 对变量的某几位进行赋值。

寄存器位经过上面的清零操作后,接下来就可以方便地对某几位写入所需要的数值了,且其它位不变, 方法见 代码清单:寄存器-11 ,这时候写入的数值一般就是需要设置寄存器的位参数。

代码清单:寄存器-11 对某几位进行赋值

 

//a = 1000 0011 b
//此时对清零后的第2组bit4、bit5设置成二进制数“01 b ”

a |= (1< <2*2);
//a = 1001 0011 b,成功设置了第2组的值,其它组不变

 

5.3.4 对变量的某位取反

某些情况下,我们需要对寄存器的某个位进行取反操作,即 1变0 ,0变1,这可以直接用如下操作,其它位不变, 见代码清单:寄存器-12 。

代码清单:寄存器-12 对某位进行取反操作

 

//a = 1001 0011 b
//把bit6取反,其它位不变

a ^=(1< <6);
//a = 1101 0011 b

 

WIZnet 是一家无晶圆厂半导体公司,成立于 1998 年。产品包括互联网处理器 iMCU™,它采用 TOE(TCP/IP 卸载引擎)技术,基于独特的专利全硬连线 TCP/IP。iMCU™ 面向各种应用中的嵌入式互联网设备。

WIZnet 在全球拥有 70 多家分销商,在香港、韩国、美国设有办事处,提供技术支持和产品营销。

香港办事处管理的区域包括:澳大利亚、印度、土耳其、亚洲(韩国和日本除外)。

审核编辑 黄宇

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分