8. 第一个实验:用寄存器点亮LED灯¶
本章内容主要参考以下的芯片硬件手册:
《RA6M5 Group User’s Manual:Hardware》
《RA4M2 Group User’s Manual:Hardware》
《RA2L1 Group User’s Manual:Hardware》
学习本章时,配合以上芯片手册中的 “19. I/O Ports” 章节一起阅读,效果会更佳,特别是涉及到寄存器说明的部分。 本章内容涉及到较多寄存器方面的深入内容,对于初学者而言这些内容丰富也较难理解,但非常有必要细读研究、夯实基础。
一般而言,点灯例程就像是刚学习C语言那时的“Hello World”程序那样,是简单入门一块单片机开发板的经典例程。 C语言的“Hello World”程序使用 printf 函数来打印“Hello World!”字符串, 而我们学习这块开发板的第一个例程的内容却不是打印“Hello World!”字符串,而是首先要学会控制芯片的引脚。 我们在后面的第19章“SCI UART——串口通讯”里才会开始使用到 printf 函数来打印字符串, 所以接下来就先来学习如何控制芯片的引脚吧!
8.1. IOPORT简介¶
芯片的引脚可以被粗略地分为 IO 引脚和非 IO 引脚。 非 IO 引脚就是电源引脚、晶振引脚等的那些引脚,他们不具备 GPIO(通用输入输出)功能。 而 IO 引脚是那些具备 GPIO 功能的引脚,他们可以配置为各种模式、实现各种通用功能。
IO 引脚最基本的输出功能是输出高、低电平,实现开关控制(比如开关LED灯、继电器或三极管等); 最基本的输入功能是检测外部输入电平(比如通过引脚电平的高低区分按键是否被按下)。 IO 引脚还可以用来连接外部设备,与外部设备进行通讯,发送控制指令,采集传感器数据等等。
IOPORT 即 I/O Port,在代码里面为了方便而写成 “IOPORT”,表示输入输出端口。 IOPORT 是 RA MCU 的一个外设模块,它用来控制芯片的引脚,对每一个引脚进行详细的配置。 具体地说,IOPORT 可以对引脚进行以下几个方面的配置:
配置引脚为普通 IO 功能,即输入或输出高电平或者低电平。
控制引脚的输入上拉电阻。
控制引脚的驱动能力。
控制引脚是否检测上升沿/下降沿/双边沿。
控制引脚是否作为中断输入引脚。
配置引脚为模拟输入功能或者将引脚在内部连接到其他外设模块。
瑞萨 RA 系列芯片的 IO 端口从理论上被分成16个组(0~9, A, B, C, D, E, F),每组有16个引脚(0~15)。 然而,实际情况并非是完整的“16个组+每组16个引脚”的配置,而是因实际的芯片型号而异。 以野火启明6M5板子上的 R7FA6M5BH3CFC 芯片为例,它是 LQFP 176-pin 封装,一共有 176 个引脚, 其中的大部分引脚都是可以被 IOPORT 模块控制的 IO 引脚。 而这些 IO 引脚被分成了 0~9, A, B 共11组引脚(注:A, B 是 10, 11 的十六进制表示), 每组一般有 16 个引脚(引脚号为0~15),实际上有些组不到16个引脚。 对于引脚数比较少的封装(LQFP 144-pin 和 LQFP 100-pin 封装)来说,芯片的可用 IO 引脚数也会相对减少。
8.2. IOPORT的框图分析¶
RA6M5、RA4M2、RA2L1 这三者的 IOPORT 模块框图结构是基本一致的。
以 RA6M5 为例,下图是 RA6M5 的外设 IOPORT 的功能结构框图,标有字母 A 处表示的是芯片实际引出的 IO 引脚。
接下来我们对 IOPORT 外设的结构框图进行分析,将不难得出 IOPORT 有如下几种工作模式:
通用输入输出(GPIO)模式
输入模式(浮空/上拉)
输出模式(推挽/开漏)
模拟输入功能模式
复用功能模式
8.2.1. IO端口方向¶
见图中标注 ① 处。
PDR (Port Direction Register) 是端口方向寄存器,它控制端口的 GPIO 方向。 当 IO 引脚需要控制输出高电平或者低电平时,可设置引脚的 GPIO 方向为 GPIO 输出; 而当需要读取 IO 引脚的电平时,可设置引脚的 GPIO 方向为 GPIO 输入。
8.2.2. IO输入上拉控制¶
见图中标注 ② 处。
PCR (Pull-up Control Register) 是上拉控制寄存器,它控制 IO 引脚的 GPIO 输入是否使能上拉。 当设置为允许上拉时,实际上会使得图中字母 B 处的弱上拉电阻连接到VCC电源正极,从而使得引脚处于弱上拉输入模式。 需要注意的是,当引脚的 GPIO 方向被配置为“输入”时,才可以设置使用弱上拉电阻。 从上图还可以看出,RA6M5 的 IO 端口是没有下拉电阻的。
8.2.3. IO驱动能力和开漏输出控制¶
见图中标注 ③ 处。
DSCR (Port Drive Capability Register) 是端口驱动能力寄存器,它控制 IO 引脚的驱动能力。 驱动能力指的是IO驱动的电流强度和IO的最大翻转速率。
NCODR (N-Channel Open-Drain Control Register) 是开漏输出控制寄存器,它控制 IO 引脚是否使能开漏模式。 当引脚的 GPIO 方向被配置为“输出”时,可以配置 IO 引脚输出的模式是推挽输出还是使能开漏输出。
8.2.4. IO端口输出数据¶
见图中标注 ④ 处。
这部分看似比较复杂,但是实际上 EOSR、POSR、PORR、EORR 的箭头最终都指向 PODR, 这意味着操作 EOSR、POSR、PORR、EORR 这些寄存器,实际上将最终操作的是 PODR 寄存器。
图中 PODR (Port Output Data Register) 是端口输出数据寄存器,它控制 GPIO 引脚输出的电平。 当引脚的 GPIO 方向被配置为“输出”时,可以配置引脚输出高电平或者低电平。
图中 EOSR (Event Output Set Register) 是事件输出置位寄存器(该寄存器我们暂且忽略它)。
图中 POSR (Pmn Output Set Register) 是端口输出数据寄存器,它控制 GPIO 引脚输出为高电平,但却不能控制输出低电平。
图中 PORR (Pmn Output Reset Register) 是端口输出数据寄存器,它控制 GPIO 引脚输出为低电平,但却不能控制输出高电平。
图中 EORR (Event Output Reset Register) 是事件输出复位寄存器(该寄存器我们暂且忽略它)。
8.2.5. IO端口输入数据¶
见图中标注 ⑤ 处。
PIDR (Port Input Data Register) 是端口输入数据寄存器,可以通过它读取 GPIO 引脚的电平状态。 当引脚的 GPIO 方向被配置为“输入”时,程序可以读出输入引脚的电平是高电平还是低电平。
8.2.6. 模拟输入模式¶
见图中标注 ⑥ 处。
ASEL (Analog Input Enable) 是模拟输入选择控制位,可以通过它来将引脚配置为模拟输入模式。 当使用 ADC 功能时,需将引脚配置为模拟输入模式。
8.2.7. 端口模式控制和外设复用选择¶
见图中标注 ⑦ 处。
PMR (Port Mode Control) 是端口模式控制位,可以通过它来将引脚配置为 GPIO 输入/输出模式,或者配置作为复用外设功能引脚。
PSEL (Peripheral Select) 是外设复用选择,可以通过它来选择将引脚连接到某一个外设功能上。
8.2.8. IO边沿检测与中断¶
见图中标注 ⑧ 处。
EOFR (Event on Falling/Event on Rising) 是事件触发选择,可以通过它来让引脚检测边沿信号,如果检测到指定信号将触发一个事件。
ISEL (IRQ Input Enable) 是IRQ输入使能控制位,可以配置是否产生中断。
8.3. IOPORT的寄存器描述¶
IOPORT 模块由于功能相对简单,寄存器相对来说比较少,下面我们就来看看它的这几个寄存器。
8.3.1. 端口引脚功能选择寄存器¶
我们在前面说过,瑞萨 RA 系列芯片的 IO 端口从理论上被分成16个组、每组有16个引脚(实际上可能会更少), 而下图中所描述的端口引脚功能选择寄存器(PmnPFS)便是用来配置 IO 引脚的,一个这样的寄存器对应配置一个 IO 引脚。
注:上图中的 “Pmn”,“m = 0 to 9, A, B”表示的是 IO 端口 m 的范围,而“n = 00 to 15”表示的是 IO 引脚 n 的范围。
PmnPFS 寄存器的位域说明如下:
位 |
位域名 |
说明 |
---|---|---|
0 |
PODR |
|
1 |
PIDR |
|
2 |
PDR |
|
4 |
PCR |
|
6 |
NCODR |
|
[11:10] |
DSCR |
|
[13:12] |
EOFR |
|
14 |
ISEL |
|
15 |
ASEL |
|
16 |
PMR |
|
[28:24] |
PSEL |
|
8.3.2. 端口输出数据寄存器¶
上图所示为端口输出数据寄存器的相关描述。该寄存器的位域说明如下:
位 |
位域名 |
说明 |
---|---|---|
[15:0] |
PDR |
|
[31:16] |
PODR |
|
8.3.3. 端口输入数据寄存器¶
上图所示为端口输入数据寄存器的相关描述。该寄存器的位域说明如下:
位 |
位域名 |
说明 |
---|---|---|
[15:0] |
PIDR |
|
[31:16] |
EIDR |
|
8.4. 实验:使用寄存器点亮LED灯¶
8.4.1. 硬件设计¶
野火启明6M5开发板的 LED 电路图如图所示。图中 RA6M5 芯片的 P400、P403、P404 引脚分别通过一个 2.2 KΩ 的限流电阻连接到 LED1、LED2、LED3 这三个用户 LED 灯的阴极,LED 灯的阳极连接到 3.3V 电源。 而 LED4 是电源指示灯,只要开发板通电就会亮。
野火启明4M2开发板的 LED 电路图下图所示。
野火启明2L1开发板的 LED 电路图下图所示。
以上所示,三块开发板的 LED 电路图差别都不大,主要差别在于用于控制用户 LED 灯的引脚不一样, 这一点在使用不同板子时需要注意一下引脚的配置。
8.4.2. 软件设计¶
8.4.2.1. 新建工程¶
- 对于 e2 studio 开发环境:
拷贝一份我们之前新建的 e2s 工程模板 “05_Template”, 然后将工程文件夹重命名为 “08_Register_LED”,最后再将它导入到我们的 e2 studio 工作空间中。
- 对于 Keil 开发环境:
拷贝一份我们之前新建的 Keil 工程模板 “06_Template”, 然后将工程文件夹重命名为 “08_Register_LED”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。
8.4.2.2. 寄存器定义头文件¶
当新建工程完成之后,工程里已经自动包含了这个定义寄存器的头文件,比如:R7FA6M5BH.h 头文件。 在这个头文件里面,已经包含了芯片所有的寄存器定义,包括 IOPORT 外设的寄存器。
以启明6M5开发板的 RA6M5 工程为例, 我们在这里列出 IOPORT 部分寄存器定义(它们存在于寄存器定义头文件 R7FA6M5BH.h 中)。
/**
* @brief I/O Ports (R_PORT0)
*/
typedef struct { /*!< (@ 0x40080000) R_PORT0 Structure */
union {
union {
__IOM uint32_t PCNTR1; /*!< (@ 0x00000000) Port Control Register 1 */
struct {
__IOM uint32_t PDR : 16; /*!< [15..0] Pmn Direction */
__IOM uint32_t PODR : 16; /*!< [31..16] Pmn Output Data */
} PCNTR1_b;
} ;
struct {
union {
__IOM uint16_t PODR; /*!< (@ 0x00000000) Output data register */
/* ... 代码过长省略 ... */
} ;
union {
__IOM uint16_t PDR; /*!< (@ 0x00000002) Data direction register */
/* ... 代码过长省略 ... */
} ;
};
};
union {
union {
__IM uint32_t PCNTR2; /*!< (@ 0x00000004) Port Control Register 2 */
struct {
__IM uint32_t PIDR : 16; /*!< [15..0] Pmn Input Data */
__IM uint32_t EIDR : 16; /*!< [31..16] Pmn Event Input Data */
} PCNTR2_b;
} ;
struct {
union {
__IM uint16_t EIDR; /*!< (@ 0x00000004) Event input data register */
/* ... 代码过长省略 ... */
} ;
union {
__IM uint16_t PIDR; /*!< (@ 0x00000006) Input data register */
/* ... 代码过长省略 ... */
} ;
};
};
union {
union {
__OM uint32_t PCNTR3; /*!< (@ 0x00000008) Port Control Register 3 */
struct {
__OM uint32_t POSR : 16; /*!< [15..0] Pmn Output Set */
__OM uint32_t PORR : 16; /*!< [31..16] Pmn Output Reset */
} PCNTR3_b;
} ;
struct {
union {
__OM uint16_t PORR; /*!< (@ 0x00000008) Output set register */
/* ... 代码过长省略 ... */
} ;
union {
__OM uint16_t POSR; /*!< (@ 0x0000000A) Output reset register */
/* ... 代码过长省略 ... */
} ;
};
};
union {
union {
__IOM uint32_t PCNTR4; /*!< (@ 0x0000000C) Port Control Register 4 */
struct {
__IOM uint32_t EOSR : 16; /*!< [15..0] Pmn Event Output Set */
__IOM uint32_t EORR : 16; /*!< [31..16] Pmn Event Output Reset */
} PCNTR4_b;
} ;
struct {
union {
__IOM uint16_t EORR; /*!< (@ 0x0000000C) Event output set register */
/* ... 代码过长省略 ... */
} ;
union {
__IOM uint16_t EOSR; /*!< (@ 0x0000000E) Event output reset register */
/* ... 代码过长省略 ... */
} ;
};
};
} R_PORT0_Type; /*!< Size = 16 (0x10) */
/**
* @brief I/O Ports-PFS (R_PFS)
*/
typedef struct { /*!< (@ 0x40080800) R_PFS Structure */
__IOM R_PFS_PORT_Type PORT[15]; /*!< (@ 0x00000000) Port [0..14] */
} R_PFS_Type; /*!< Size = 960 (0x3c0) */
/**
* @brief I/O Ports-MISC (R_PMISC)
*/
typedef struct { /*!< (@ 0x40080D00) R_PMISC Structure */
union {
__IOM uint8_t PFENET; /*!< (@ 0x00000000) Ethernet Control Register */
/* ... 代码过长省略 ... */
} ;
__IM uint8_t RESERVED[2];
union {
__IOM uint8_t PWPR; /*!< (@ 0x00000003) Write-Protect Register */
/* ... 代码过长省略 ... */
} ;
__IM uint8_t RESERVED1;
union {
__IOM uint8_t PWPRS; /*!< (@ 0x00000005) Write-Protect Register for Secure */
/* ... 代码过长省略 ... */
} ;
__IM uint16_t RESERVED2[5];
__IOM R_PMISC_PMSAR_Type PMSAR[12]; /*!< (@ 0x00000010) Port Security Attribution Register */
} R_PMISC_Type; /*!< Size = 40 (0x28) */
/** @addtogroup Device_Peripheral_peripheralAddr
* @{ 外设首地址
*/
#define R_PORT0_BASE 0x40080000UL
#define R_PORT1_BASE 0x40080020UL
#define R_PORT2_BASE 0x40080040UL
#define R_PORT3_BASE 0x40080060UL
#define R_PORT4_BASE 0x40080080UL
#define R_PORT5_BASE 0x400800A0UL
#define R_PORT6_BASE 0x400800C0UL
#define R_PORT7_BASE 0x400800E0UL
#define R_PORT8_BASE 0x40080100UL
#define R_PORT9_BASE 0x40080120UL
#define R_PORT10_BASE 0x40080140UL
#define R_PORT11_BASE 0x40080160UL
#define R_PORT12_BASE 0x40080180UL
#define R_PORT13_BASE 0x400801A0UL
#define R_PORT14_BASE 0x400801C0UL
#define R_PFS_BASE 0x40080800UL
#define R_PMISC_BASE 0x40080D00UL
/** @addtogroup Device_Peripheral_declaration
* @{ 外设寄存器声明(定义结构体指针,指向 IOPORT 寄存器首地址)
*/
#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)
#define R_PORT11 ((R_PORT0_Type*) R_PORT11_BASE)
#define R_PORT12 ((R_PORT0_Type*) R_PORT12_BASE)
#define R_PORT13 ((R_PORT0_Type*) R_PORT13_BASE)
#define R_PORT14 ((R_PORT0_Type*) R_PORT14_BASE)
#define R_PFS ((R_PFS_Type*) R_PFS_BASE)
#define R_PMISC ((R_PMISC_Type*) R_PMISC_BASE)
8.4.2.3. hal_entry入口函数¶
一般来说,接下来我们应该在 main 函数里编写我们的程序, 但是使用 FSP 库却不一样,在没有使用 RTOS 的情况下,它规定以名为 hal_entry 的函数作为用户应用程序的入口, 因此我们应该在 hal_entry 入口函数下编写我们的代码。
实际上,当使用 RTOS 时,程序是从 main 函数开始进行线程调度; 当没有使用 RTOS 时,C语言程序的入口函数 main 函数调用了 hal_entry 函数。 我们新建的工程是没有选用 RTOS 的,因此,用户程序是从 hal_entry 函数开始执行。 我们打开 “\src\hal_entry.c” 文件,在 hal_entry 函数里面编写我们的代码。
以启明6M5开发板为例,RA6M5 工程的 hal_entry 函数代码如下所示。
注解
启明4M2开发板和启明2L1开发板的用户可直接打开配套的“08_Register_LED”例程查看该代码,限于篇幅,不在本章中贴出。
void hal_entry(void)
{
/* TODO: add your own code here */
/* 取消写保护 */
R_PMISC->PWPR = 0; ///< Clear BOWI bit - writing to PFSWE bit enabled
R_PMISC->PWPR = 1U << BSP_IO_PWPR_PFSWE_OFFSET; ///< Set PFSWE bit - writing to PFS register enabled
/* LED1:配置引脚 P400 对应的PFS寄存器 */
R_PFS->PORT[BSP_IO_PORT_04_PIN_00>>8].PIN[BSP_IO_PORT_04_PIN_00 & 0xFF].PmnPFS =
IOPORT_CFG_PORT_DIRECTION_OUTPUT | IOPORT_CFG_PORT_OUTPUT_LOW;
/* LED2:配置引脚 P403 对应的PFS寄存器 */
R_PFS->PORT[BSP_IO_PORT_04_PIN_03>>8].PIN[BSP_IO_PORT_04_PIN_03 & 0xFF].PmnPFS =
IOPORT_CFG_PORT_DIRECTION_OUTPUT | IOPORT_CFG_PORT_OUTPUT_LOW;
/* LED3:配置引脚 P404 对应的PFS寄存器 */
R_PFS->PORT[BSP_IO_PORT_04_PIN_04>>8].PIN[BSP_IO_PORT_04_PIN_04 & 0xFF].PmnPFS =
IOPORT_CFG_PORT_DIRECTION_OUTPUT | IOPORT_CFG_PORT_OUTPUT_LOW;
/** 此时3个LED灯的引脚默认输出的是低电平
* 所以3个LED灯都会默认亮起来
* 我们在 while 循环里让 LED1 闪烁:每秒钟翻转一次状态
*/
while(1)
{
/* 翻转LED灯:LED1 */
//R_PORT4->PODR |= 1<<(BSP_IO_PORT_04_PIN_00 & 0xFF);
//R_BSP_SoftwareDelay(1000, BSP_DELAY_UNITS_MILLISECONDS);
//R_PORT4->PODR &= (uint16_t)~(1 << (BSP_IO_PORT_04_PIN_00 & 0xFF));
//R_BSP_SoftwareDelay(1000, BSP_DELAY_UNITS_MILLISECONDS);
/* 或者也可以这样用位异或操作来翻转LED1 */
R_PORT4->PODR ^= 1<<(BSP_IO_PORT_04_PIN_00 & 0xFF);
R_BSP_SoftwareDelay(1000, BSP_DELAY_UNITS_MILLISECONDS);
}
//这后面的代码无需理会
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
8.4.3. 下载验证¶
编写好上述代码,然后将程序编译并下载到开发板之后,按下复位按键来复位开发板, 可以观察到开发板上面除了电源指示灯之外的3个 LED 灯当中有两个灯常亮,还有一个灯在缓慢闪烁。 闪烁着的 LED 灯为 LED1,它每秒钟(1000毫秒)便改变一次亮灭的状态。