4. 初识寄存器

本章配套视频介绍:

../../_images/video.png

《07-初识寄存器(第1节)——什么是寄存器》

https://www.bilibili.com/video/BV1dX4y1J7f4/

../../_images/video.png

《08-初识寄存器(第2节)——寄存器的映射》

https://www.bilibili.com/video/BV1CV411L7Fm/

../../_images/video.png

《09-初识寄存器(第3节)——使用寄存器点亮LED》

https://www.bilibili.com/video/BV1zc411F7hx/

4.1. 寄存器是什么

寄存器实际上与 RAM、FLASH 一样,也是芯片内部的一种存储器(Memory)。 一般而言,RAM 是程序运行的内存,FLASH 则是用来保存程序本身。 寄存器与 RAM、FLASH 等存储器的不同之处在于:寄存器除了保存了芯片的功能状态之外, 还是配置和控制芯片的桥梁,我们可以通过寄存器配置和操作芯片的功能。

一般而言,我们在对 MCU 芯片进行编程时有两种编程方式, 一种是寄存器编程,另外一种是固件库编程(或者说库函数编程)。 那么,固件库又是什么东西?固件库说白了其实是通过寄存器编程之后的产物, 它是对寄存器操作的一种封装,最终提供给开发者一套固定的函数API进行调用。

我们可以从以下两种角度来了解寄存器编程与固件库编程的区别。 从程序执行效率的角度来看:一般而言,寄存器编程生成的程序执行效率高, 而固件库编程生成的程序执行效率不如寄存器编程的。 然而从开发者的角度来看:固件库编程使得开发者不必深入理解硬件层面的寄存器细节, 在开发时只需要调用库函数以实现所需的功能,因此可以提高开发者的开发效率。

4.2. 瑞萨RA芯片里面有什么

在知道有寄存器这个东西存在后,还需要通过瑞萨官方的芯片数据手册了解它里面有什么, 知道了芯片内部的结构之后,也就知道如何通过寄存器对芯片进行编程了。 所以我们先来看看 RA 系列芯片内部有些什么。

简单来讲,MCU 芯片里面主要有两大部分,一是CPU内核,二是片上外设。 以 RA6M5 芯片为例,RA6M5 所采用的CPU内核是 Cortex-M33(简称CM33)。 该CPU内核由ARM公司设计,但其实ARM公司并不生产芯片,而是出售其芯片技术授权。 芯片生产厂商,比如 Renesas、ST、NXP、TI 等等,他们负责在CPU内核之外设计各个模块并生产整个芯片, 这些内核之外的模块被称为 “核外设备”“片上外设” (Peripheral)。 例如,RA6M5 芯片内部的外设模块:I/O Ports(GPIO)、SCI(串口)、I2C、SPI 等等,这些都叫做片上外设。

实际上,既然有 “核外设备”,那必然也有 “核内设备”,即:CPU 内核(Cortex-M33)内部也是具有一定的设备模块的结构的。 例如,CPU 内部有 NVIC(嵌套向量中断控制器)、FPU(浮点计算单元)等等。

如下图所示,展示了 RA6M5 芯片内部模块与资源:

图

上图中,我们可以看到有一个标着 “Arm Cortex-M33” 的方框,其所表示的便是 CPU 内核, 其中包含的小方框(DSP、FPU、MPU、NVIC等)属于内核的设备。

除了 “Arm Cortex-M33” 的方框以外,还有很多个大方框,它们对片上的全部外设模块进行了一个分类, 大方框当中的小方框表示的是外设模块,如下:

外设模块及其分类

类别

外设模块

存储器模块(Memory)

Flash、SRAM

直接内存访问(DMA)

DTC、DMAC

总线(Bus)

CSC、MPU

系统(System)

POR/LVD、Reset、Mode control、Power control、

ICU、Clocks、CAC、

Battery backup、Register write protection

定时器(Timers)

GPT、AGT、RTC、WDT/IWDT

通讯接口(Communication interfaces)

SCI、IIC、SPI、QSPI、OSPI、SDHI、ETHERC、

CAN-FD、USBHS、USBFS、CEC

人机交互接口(Human machine interface)

CTSU

模拟(Analog)

ADC、DAC、TSN

数据处理(Data processing)

CRC、DOC

事件链接(Event link)

ELC

安全(Security)

SCE9

可以看到,芯片里面的外设模块有很多。其中部分外设模块是相对简单的,而部分则是非常复杂。 本教程的大部分篇章都是在讲解这些外设模块,我们会由简入难,逐步的了解和使用它们。

CPU 内核结构是复杂的,但是我们不需要细究。 对于一般嵌入式开发来说,需要了解的CPU内核的模块其实很少,重要的只有NVIC、SysTick等,而我们会在后面进行详细介绍。

RA6M5 芯片 Cortex-M33 CPU 内核结构如图所示:

图

4.3. 存储器映射

前文所述,寄存器与 RAM、FLASH 一样都是芯片内部的一种存储设备。 那么,当我们需要访问它们的时候,我们需要知道它们的存储地址。

4.3.1. 存储器映射表

如下图所示为 RA6M5 的存储器映射表,可以看到 RA6M5 芯片内部的存储器被映射到这一整块 4G(0 ~ 0xFFFF FFFF)的地址空间中。 我们还可以看到,除了寄存器和 SRAM、Flash 的地址空间区域以外,还存在着其他类型的地址空间区域,比如 QSPI area 和 OSPI area。 Reserved area 表示的是保留区域,尚未用到。

Memory map

4.3.2. 存储器区域划分

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

对于 RA6M5 (176 pin) 芯片,其内部线性地址空间划分为如下区域:

线性地址空间区域划分

区域地址范围

区域用途

0x0000_0000 ~ 0x0030_0000 - 1

On-chip flash (code flash):片上Flash(代码)

0x0100_80F0 ~ 0x0100_81B4 - 1

On-chip flash (Factory flash):片上Flash(出厂保留)

0x0100_A100 ~ 0x0100_A300 - 1

On-chip flash (option-setting flash):片上Flash(选项设置)

0x0800_0000 ~ 0x0800_2000 - 1

On-chip flash (data flash):片上Flash(数据)

0x2000_0000 ~ 0x2008_0000 - 1

SRAM0

0x2800_0000 ~ 0x2800_0400 - 1

Stanby SRAM

0x4000_0000 ~ 0x4018_0000 - 1

Peripheral I/O registers:外设寄存器

0x407E_0000 ~ 0x407F_0000 - 1

Flash I/O registers:Flash寄存器

0x407F_C000 ~ 0x4080_0000 - 1

Flash I/O registers:Flash寄存器

0x6000_0000 ~ 0x6800_0000 - 1

External address space (Quad SPI area):外部QSPI存储器地址

0x6800_0000 ~ 0x8000_0000 - 1

External address space (Ouad SPI area):外部OSPI存储器地址

0x8000_0000 ~ 0x8800_0000 - 1

External address space (CS area):外部存储器地址

0xE000_0000 ~ 0xFFFF_FFFF

System for Cortex-M33:处理器内核设备寄存器

表格中的 “0x4000_0000 ~ 0x4018_0000 - 1” 区域,也就是 “0x4000_0000 ~ 0x4017_FFFF” 区域, 它映射到了绝大部分外设模块的寄存器。

4.3.3. 外设基地址和外设寄存器地址

打开参考文档《RA6M5 Group User’s Manual:Hardware》, 在第19章“I/O Ports”当中的第2小节为“Register Descriptions”,此小节详细描述了外设模块 IOPORT(即GPIO)的寄存器。 我们截取一部分内容来说明,如下图所示。

图

图中①处为该外设的基地址,也就是IO端口的基地址。 因为 RA6M5 的IO端口不止有一个,而是有16个端口(用 PORTm 表示,m = 0~9, A, B), 所以每一个端口都有一个基地址,每个端口的基地址都可以用图中的公式来计算出来。

图中②处为该外设寄存器的地址偏移,图中的寄存器为 PCNTR1/PODR/PDR 寄存器, 而“Offset address: 0x000”表示的是该寄存器相对于基地址的偏移量。

举例来说,当我们要读取 PORT1 的 PCNTR1/PODR/PDR 寄存器的值时, 我们要先计算出该寄存器的地址为:(0x40080000 + 0x0020*1), 然后再把该地址值转换为C语言的指针:(uint32_t *)(0x40080000 + 0x0020*1), 最后再取值即可读出该寄存器的值:*( (uint32_t *)(0x40080000 + 0x0020*1) )。

需要注意的是,每一种外设模块下面都会有多个寄存器,每个寄存器都有特定的功能。 对于一些功能相对复杂的外设来说,它们的寄存器数量可以达到十几个甚至几十个。 以 IOPORT1 为例,它的基地址为:0x40080020,下表则展示了它部分的寄存器名称、寄存器地址以及相对于基地址的偏移。

IOPORT1 寄存器及其地址

寄存器名称

寄存器地址

相对于基地址的偏移

PCNTR1/PODR/PDR

0x40080020

0x000

PCNTR2/EIDR/PIDR

0x40080024

0x004

PCNTR3/PORR/POSR

0x40080028

0x008

PCNTR4/EORR/EOSR

0x4008002C

0x00C

注解

注:由于基地址不同,上述表格未包含 PmnPFS 等这些也和 IOPORT1 有关的寄存器。

4.3.4. 外设寄存器

下图所示为在文档《RA6M5 Group User’s Manual:Hardware》中描述外设寄存器的一般格式。

图

说明:

  • ①寄存器名称。

  • ②外设模块基地址及其寄存器偏移地址。

  • ③寄存器位表格。32位MCU的寄存器大小一般为 32 位(bit),占四个字节。 “Bit position”为位号,指示该位处于该寄存器中的位置; “Bit field”为位域,一般不同的位域有不同的作用; “Value after reset”为复位值,指示该位的复位值。

  • ④位域功能说明。这部分为对每一个位域的功能的详细说明。

4.4. 如何用C语言操作寄存器

4.4.1. C语言对寄存器的封装

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

4.4.1.1. 外设模块基地址定义

在编程上为了方便理解和记忆,我们要把外设模块基地址以相应的宏定义起来,外设基地址都以它们的名字作为宏名的组成部分。 以下是 IO 端口外设基地址的宏定义。

代码清单 4‑1 IOPORT 外设基地址宏定义
/* 外设基地址 */
#define R_PORT0_BASE          0x40080000
#define R_PORT1_BASE          0x40080020
#define R_PORT2_BASE          0x40080040
#define R_PORT3_BASE          0x40080060
#define R_PORT4_BASE          0x40080080
#define R_PORT5_BASE          0x400800A0
#define R_PORT6_BASE          0x400800C0
#define R_PORT7_BASE          0x400800E0
#define R_PORT8_BASE          0x40080100
#define R_PORT9_BASE          0x40080120
#define R_PORT10_BASE         0x40080140
#define R_PORT11_BASE         0x40080160
#define R_PFS_BASE            0x40080800
#define R_PMISC_BASE          0x40080D00

4.4.1.2. 寄存器结构体定义

由于寄存器的数量是非常之多的,如果每个寄存器都用像 *( (uint32_t *)(0x40080000 + 0x0020*1) ) 这样的方式去访问的话,会显得很繁琐、很麻烦。 为了更方便地访问寄存器,我们会借助C语言结构体的特性去定义寄存器和寄存器位域,这是通用的做法。

代码清单 4‑2 使用结构体封装外设寄存器
// 注:关于输入输出端口的声明
/* C语言: IO definitions (access restrictions to peripheral registers) */
//#define     __I     volatile const       /*!< Defines 'read only' permissions */
//#define     __O     volatile             /*!< Defines 'write only' permissions */
//#define     __IO    volatile             /*!< Defines 'read / write' permissions */

/* 下面的宏定义用于结构体成员 */
/* following defines should be used for structure members */
//#define     __IM     volatile const      /*! Defines 'read only' structure member permissions */
//#define     __OM     volatile            /*! Defines 'write only' structure member permissions */
//#define     __IOM    volatile            /*! Defines 'read / write' structure member permissions */

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

/**
* @brief I/O Ports (R_PORT0)
*/
typedef struct                         /*!< (@ 0x40040000) R_PORT0 Structure */
{
   union
   {
      union
      {
            __IOM uint32_t PCNTR1;        /*!< (@ 0x00000000) Port Control Register 1 */

            struct
            {
               __IOM uint32_t PDR  : 16; /*!< [15..0] Pmn Direction(引脚Pmn 方向)*/
               __IOM uint32_t PODR : 16; /*!< [31..16] Pmn Output Data(引脚Pmn 输出数据)*/
            } PCNTR1_b;
      };

      /* ... 代码过长省略 ... */
   };

   union
   {
      union
      {
            __IM uint32_t PCNTR2;        /*!< (@ 0x00000004) Port Control Register 2 */

            struct
            {
               __IM uint32_t PIDR : 16; /*!< [15..0] Pmn Input Data(引脚Pmn 输入数据)*/
               __IM uint32_t EIDR : 16; /*!< [31..16] Pmn Event Input Data(引脚Pmn 事件输入数据)*/
            } PCNTR2_b;
      };

      /* ... 代码过长省略 ... */
   };

   union
   {
      union
      {
            __OM uint32_t PCNTR3;        /*!< (@ 0x00000008) Port Control Register 3 */

            struct
            {
               __OM uint32_t POSR : 16; /*!< [15..0] Pmn Output Set(引脚Pmn 输出置位)*/
               __OM uint32_t PORR : 16; /*!< [31..16] Pmn Output Reset(引脚Pmn 输出复位)*/
            } PCNTR3_b;
      };

      /* ... 代码过长省略 ... */
   };

   union
   {
      union
      {
            __IOM uint32_t PCNTR4;        /*!< (@ 0x0000000C) Port Control Register 4 */

            struct
            {
               __IOM uint32_t EOSR : 16; /*!< [15..0] Pmn Event Output Set(引脚Pmn 事件输出置位)*/
               __IOM uint32_t EORR : 16; /*!< [31..16] Pmn Event Output Reset(引脚Pmn 事件输出复位)*/
            } PCNTR4_b;
      };

      /* ... 代码过长省略 ... */
   };
} R_PORT0_Type;                        /*!< Size = 16 (0x10) */

4.4.1.3. 外设模块寄存器定义

我们在上一步已经定义好了 R_PORT0_Type 类型的结构体,它包含了 IOPORT 的寄存器定义。 接下来使用宏定义来表示结构体指针,指针指向 IOPORT 外设的每个端口的寄存器首地址。

代码清单 4‑3 寄存器定义
#define R_PORT0           ((R_PORT0_Type *) R_PORT0_BASE)
#define R_PORT1           ((R_PORT0_Type *) R_PORT1_BASE)
#define R_PORT2           ((R_PORT0_Type *) R_PORT2_BASE)
#define R_PORT3           ((R_PORT0_Type *) R_PORT3_BASE)
#define R_PORT4           ((R_PORT0_Type *) R_PORT4_BASE)
#define R_PORT5           ((R_PORT0_Type *) R_PORT5_BASE)
#define R_PORT6           ((R_PORT0_Type *) R_PORT6_BASE)
#define R_PORT7           ((R_PORT0_Type *) R_PORT7_BASE)
#define R_PORT8           ((R_PORT0_Type *) R_PORT8_BASE)
#define R_PORT9           ((R_PORT0_Type *) R_PORT9_BASE)
#define R_PORT10          ((R_PORT0_Type *) R_PORT10_BASE)

这样便大功告成了,我们就可以使用这些宏来访问各个 IO 端口的每一个寄存器了。

4.4.2. 修改寄存器操作的本质:读-改-写

有了以上的对 IOPORT 这个外设模块的寄存器的定义, 我们便完成了“C语言对寄存器的封装”这个步骤,接下来我们便可以使用C语言对寄存器进行各种操作了。

对寄存器进行操作可以是忽略寄存器原本的值,而直接覆盖写入新的值; 但是更为一般的操作是根据原本的寄存器值进行修改,即:先读出寄存器原本的值,然后修改该值,最后重新写入到寄存器里面, 让新的值生效。

接下来将介绍修改寄存器的几种通用方法。

4.4.2.1. 清零寄存器上的某 N 个位

使用 C 语言的按位与 “&” 运算符可以将位进行清零。

代码清单 4‑4 位清零:按位与 &
//清零某个位
R_PORT0->PODR &= ~(1u<<0); //清零PODR寄存器的第0位
R_PORT0->PODR &= ~(1u<<6); //清零PODR寄存器的第6位

//清零多个位
R_PORT0->PODR &= ~(3u<<0); //清零PODR寄存器的第0,1位
R_PORT0->PODR &= ~(3u<<6); //清零PODR寄存器的第6,7位

4.4.2.2. 对寄存器上的某 N 个位进行置位

使用 C 语言的按位或 “|” 运算符可以将位进行置一。

代码清单 4‑5 位置位:按位或 |
//置位某个位
R_PORT0->PODR |= 1u<<0; //PODR寄存器的第0位置1
R_PORT0->PODR |= 1u<<6; //PODR寄存器的第6位置1

//置位多个位
R_PORT0->PODR |= 3u<<0; //PODR寄存器的第0,1位置1
R_PORT0->PODR |= 3u<<6; //PODR寄存器的第6,7位置1

4.4.2.3. 对寄存器上的某 N 个位进行取反

使用 C 语言的按位异或 “^” 运算符可以将位进行取反。

代码清单 4‑6 位取反:按位异或 ^
//取反某个位
R_PORT0->PODR ^= 1u<<0; //取反PODR寄存器的第0位
R_PORT0->PODR ^= 1u<<6; //取反PODR寄存器的第6位

//取反多个位
R_PORT0->PODR ^= 3u<<0; //取反PODR寄存器的第0,1位
R_PORT0->PODR ^= 3u<<6; //取反PODR寄存器的第6,7位