9. 自己写库——构建库函数雏形

本章配套视频介绍:

../../_images/video.png

《10-自己写库–构件库函数雏形(第1节)——编写头文件》

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

../../_images/video.png

《11-自己写库–构建库函数雏形(第2节)——编写源文件》

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

实际上,构建固件库是一件费时费力的事情,并且它对开发者对芯片的熟悉程度有一定的要求。 甚至,当一个固件库的封装程度很高时,想要阅读并理解该固件库的底层代码也会变成一件有较高难度的事情。 瑞萨 RA 系列单片机所使用的官方 FSP 库便是这样的一种封装程度很高的固件库。 所幸的是,FSP 库的意义在于为开发者封装硬件底层,因此一般而言,FSP 库是带来方便的, 开发者只需学会如何使用 FSP 库即可,而不用深入到 FSP 库的底层细节。

构建固件库一般是由芯片厂商来做,比如 FSP 库就是由瑞萨官方构建和维护的。 为了让读者更加清晰地了解到固件库的作用,我们接下来将会说明一种构建一个封装程度较低的固件库的思路。 值得注意的是,本章所构建的库函数雏形与实际上的 FSP 库有很大区别,但求这对读者理解和使用 FSP 库有所帮助。

9.1. 硬件设计

野火启明6M5开发板的 LED 电路图如图所示。

图

野火启明4M2开发板的 LED 电路图下图所示。

图

野火启明2L1开发板的 LED 电路图下图所示。

图

9.2. 软件设计

9.2.1. 新建工程

对于 e2 studio 开发环境:

拷贝一份我们之前新建的 e2s 工程模板 “05_Template”, 然后将工程文件夹重命名为 “09_Register_MyLib”,最后再将它导入到我们的 e2 studio 工作空间中。

对于 Keil 开发环境:

拷贝一份我们之前新建的 Keil 工程模板 “06_Template”, 然后将工程文件夹重命名为 “09_Register_MyLib”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。

工程新建好之后,在工程根目录的 “src” 文件夹下面新建 “ioport” 文件夹, 再进入 “ioport” 文件夹里面新建 ioport 驱动的源文件和头文件:“ra_ioport.c” 和 “ra_ioport.h”。 工程文件结构如下。

文件结构
09_Register_MyLib
├─ ......
└─ src
   ├─ ioport
   │  ├─ ra_ioport.c
   │  └─ ra_ioport.h
   └─ hal_entry.c

警告

注意:对于使用 Keil 开发环境的用户,将代码文件放到 “src” 文件夹下之后, Keil 软件并不会自动将它们加入到工程,这时候需要打开 RASC FSP 配置界面, 点击一次单击右上角的 “Generate Project Content” 按钮,从而 “src” 文件夹下的代码文件就会被自动加入进工程中。 接着关闭 FSP 配置界面返回到 Keil,然后进行一次编译后会弹出一个提示框提示工程结构发生了变化,点击确定即可。 对于使用 e2 studio 的用户则不需要如此。

9.2.2. 包含寄存器定义的头文件

在 ra_ioport.h 中通过包含 “hal_data.h” 头文件来间接包含寄存器定义头文件: R7FA6M5BH.hR7FA4M2AD.hR7FA2L1AB.h

代码清单 9-1:ra_ioport.h
#include "hal_data.h"   //间接包含了头文件 "R7FA6M5BH.h" / "R7FA4M2AD.h" / "R7FA2L1AB.h"

9.2.3. 端口和引脚号的枚举类型定义

我们需要定义端口和引脚号的枚举类型,用来表示某个要操控的某个引脚。 但是如果像:“BSP_IO_PORT_04_PIN_00”、“BSP_IO_PORT_04_PIN_03” 和 “BSP_IO_PORT_04_PIN_04” 这样定义端口和引脚号的枚举类型,会造成与 FSP 库中的枚举定义冲突,因此我们暂时把端口和引脚号分开出来单独定义。

代码清单 9-2:ra_ioport.h 文件
/* IOPORT 端口定义 */
typedef enum io_port
{
   IO_PORT_00 = 0x0000,           ///< IO port 0
   IO_PORT_01 = 0x0100,           ///< IO port 1
   IO_PORT_02 = 0x0200,           ///< IO port 2
   IO_PORT_03 = 0x0300,           ///< IO port 3
   IO_PORT_04 = 0x0400,           ///< IO port 4
   IO_PORT_05 = 0x0500,           ///< IO port 5
   IO_PORT_06 = 0x0600,           ///< IO port 6
   IO_PORT_07 = 0x0700,           ///< IO port 7
   IO_PORT_08 = 0x0800,           ///< IO port 8
   IO_PORT_09 = 0x0900,           ///< IO port 9
   IO_PORT_10 = 0x0A00,           ///< IO port 10
   IO_PORT_11 = 0x0B00,           ///< IO port 11
   IO_PORT_12 = 0x0C00,           ///< IO port 12
   IO_PORT_13 = 0x0D00,           ///< IO port 13
   IO_PORT_14 = 0x0E00,           ///< IO port 14
} IO_Port_t;

/* IOPORT 引脚定义 */
typedef enum io_pin
{
   IO_PIN_00 = 0x0000,    ///< IO port 0 pin 0
   IO_PIN_01 = 0x0001,    ///< IO port 0 pin 1
   IO_PIN_02 = 0x0002,    ///< IO port 0 pin 2
   IO_PIN_03 = 0x0003,    ///< IO port 0 pin 3
   IO_PIN_04 = 0x0004,    ///< IO port 0 pin 4
   IO_PIN_05 = 0x0005,    ///< IO port 0 pin 5
   IO_PIN_06 = 0x0006,    ///< IO port 0 pin 6
   IO_PIN_07 = 0x0007,    ///< IO port 0 pin 7
   IO_PIN_08 = 0x0008,    ///< IO port 0 pin 8
   IO_PIN_09 = 0x0009,    ///< IO port 0 pin 9
   IO_PIN_10 = 0x000A,    ///< IO port 0 pin 10
   IO_PIN_11 = 0x000B,    ///< IO port 0 pin 11
   IO_PIN_12 = 0x000C,    ///< IO port 0 pin 12
   IO_PIN_13 = 0x000D,    ///< IO port 0 pin 13
   IO_PIN_14 = 0x000E,    ///< IO port 0 pin 14
   IO_PIN_15 = 0x000F,    ///< IO port 0 pin 15
} IO_Pin_t;

9.2.4. IOPORT相关功能的枚举类型定义

接着定义 IOPORT 相关功能的枚举类型,这里的每一个枚举类型都对应着 IOPORT 的其中一项功能配置。

代码清单 9-3:ra_ioport.h 文件
/* IO 引脚模式 */
typedef enum
{
   IO_MODE_GPIO = 0,   /* 普通 GPIO 功能 */
   IO_MODE_AF   = 1,   /* 复用功能 */
   IO_MODE_AN   = 2    /* 模拟输入输出功能 */
} IO_Mode_t;


/* IO 引脚方向 */
typedef enum
{
   IO_DIR_INPUT  = 0,  // 引脚用作GPIO输入功能
   IO_DIR_OUTPUT = 1   // 引脚用作GPIO输出功能
} IO_Dir_t;


/* IO 引脚输出类型 */
typedef enum
{
   IO_OTYPE_PP = 0x00, /* 推挽模式 */
   IO_OTYPE_OD = 0x01  /* 开漏模式 */
} IO_OType_t;


/* IO 引脚输出能力 */
typedef enum
{
   IO_DRIVE_LOW    = 0,    // Low drive
   IO_DRIVE_MIDDLE = 1,    // Middle drive
   IO_DRIVE_HSHD   = 2,    // High-speed high-drive
   IO_DRIVE_HIGH   = 3     // High drive
} IO_DriveCapability_t;


/* IO 引脚输出电平 */
typedef enum io_level
{
   IO_LEVEL_LOW = 0,       // Low
   IO_LEVEL_HIGH           // High
} IO_Level_t;


/* IO 引脚输入上下拉 */
typedef enum
{
   IO_NO_PULL =   0x00u,   /* 浮空输入 */
   IO_PULL_UP =   0x01u,   /* 上拉输入 */
   //IO_PULL_DOWN = 0x02u  /* RA6M5/RA4M2/RA2L1 引脚没有下拉功能 */
} IO_Pull_t;

9.2.5. 定义IO初始化结构体

由上述 IOPORT 相关功能的枚举类型我们可以知道,在对 IOPORT 模块进行初始化时需要根据情况配置它们。 因此我们定义一个 IOPORT 初始化的结构体类型 IOPORT_Init_t, 它的成员包括了由上述所有枚举类型所声明的变量,因此该结构体类型的变量可以包含 IOPORT 的相关功能配置。

代码清单 9-4:ra_ioport.h 文件
/* IOPORT 初始化结构体类型定义 */

typedef struct
{
   IO_Port_t               Port;
   IO_Pin_t                Pin;
   IO_Mode_t               Mode;
   IO_Dir_t                Dir;
   IO_OType_t              OType;
   IO_DriveCapability_t    Drive;
   IO_Level_t              Level;
   IO_Pull_t               Pull;
} IOPORT_Init_t;

9.2.6. 编写IO操作函数

我们把 IO 操作函数的声明和 IO 初始化函数的声明都放在 ra_ioport.h 头文件。

代码清单 9-5:ra_ioport.h 文件
/* IO 操作函数(调用一次只能操作一个IO引脚) */
uint32_t IOPORT_PinRead         (IO_Port_t port, IO_Pin_t pin);
void     IOPORT_PinWrite        (IO_Port_t port, IO_Pin_t pin, IO_Level_t level);
void     IOPORT_PinToggle       (IO_Port_t port, IO_Pin_t pin);
void     IOPORT_PinAccessEnable (void);
void     IOPORT_PinAccessDisable (void);

/* IO 初始化函数(调用一次只能初始化一个IO引脚) */
void     IOPORT_Init (IOPORT_Init_t *ioport_init);

然后在 ra_ioport.c 源文件里面实现这些 IO 操作函数。

代码清单 9-6:ra6m5_ioport.c 文件
/* 读引脚电平 */

uint32_t IOPORT_PinRead(IO_Port_t port, IO_Pin_t pin)
{
   /* Read pin level. */
   return R_PFS->PORT[port >> 8].PIN[pin].PmnPFS_b.PIDR;
}


/* 写引脚电平 */

void IOPORT_PinWrite(IO_Port_t port, IO_Pin_t pin, IO_Level_t level)
{
   uint32_t pfs_bits = R_PFS->PORT[port >> 8].PIN[pin].PmnPFS; //读寄存器 PmnPFS

   pfs_bits &= ~(uint32_t)0x1; //清零 PODR 位

   R_PFS->PORT[port >> 8].PIN[pin].PmnPFS = (pfs_bits | level);
}


/* 翻转引脚电平 */

void IOPORT_PinToggle(IO_Port_t port, IO_Pin_t pin)
{
   uint32_t pfs_bits = R_PFS->PORT[port >> 8].PIN[pin].PmnPFS; //读寄存器 PmnPFS

   pfs_bits ^= (uint32_t)0x1; //取反 PODR 位

   R_PFS->PORT[port >> 8].PIN[pin].PmnPFS = pfs_bits;
}


/* 引脚访问使能 */

void IOPORT_PinAccessEnable(void)
{
   R_PMISC->PWPR = 0;        ///< Clear BOWI bit - writing to PFSWE bit enabled
   R_PMISC->PWPR = 1U << 6U; ///< Set PFSWE bit - writing to PFS register enabled
}


/* 引脚访问禁止 */

void IOPORT_PinAccessDisable(void)
{
   R_PMISC->PWPR = 0;        ///< Clear PFSWE bit - writing to PFS register disabled
   R_PMISC->PWPR = 1U << 7U; ///< Set BOWI bit - writing to PFSWE bit disabled
}

9.2.7. 编写IO初始化函数

最后编写 IOPORT 初始化函数。

代码清单 9-7:ra_ioport.c 文件
/* IOPORT 初始化函数 */

void IOPORT_Init(IOPORT_Init_t *ioport_init)
{
   uint32_t pfs_bits = 0; //不读取寄存器 PmnPFS

   if(ioport_init->Mode == IO_MODE_GPIO) //如果引脚用作普通GPIO功能
   {
      if(ioport_init->Dir == IO_DIR_INPUT)        /* 用作输入模式 */
      {
         pfs_bits |= (ioport_init->Pull) << 4;   //设置输入上拉
      }
      else if(ioport_init->Dir == IO_DIR_OUTPUT)  /* 用作输出模式 */
      {
         pfs_bits |= (ioport_init->Dir) << 2;    //设置为输出
         pfs_bits |= (ioport_init->Level) << 0;  //设置输出电平
         pfs_bits |= (ioport_init->Mode) << 6;   //设置输出模式
         pfs_bits |= (ioport_init->Drive) << 10; //设置输出驱动能力
      }
   }
   else
   {
      //我们不考虑引脚用作复用模式和模拟输入输出模式的情况
      //也不考虑中断的情况,仅考虑普通的GPIO输入输出功能
   }

   /* 写入配置到寄存器 PmnPFS */
   R_PFS->PORT[ioport_init->Port >> 8].PIN[ioport_init->Pin].PmnPFS = pfs_bits;
}

9.2.8. hal_entry入口函数

前一章节实验里有讲过,当使用 RTOS 时,程序从 main 函数开始进行线程调度; 当没有使用 RTOS 时,C语言程序的入口函数 main 函数调用了 hal_entry 函数。 本章节实验的工程也是没有选用 RTOS 的,因此,用户程序是从 hal_entry 函数开始执行。

打开 “\src\hal_entry.c” 文件, 我们在 “hal_entry.c” 文件中添加对头文件 “ra_ioport.h” 的包含, 然后在 hal_entry 函数里面编写我们的代码。

以启明6M5开发板为例,RA6M5 工程的 hal_entry 函数代码如下所示。

注解

启明4M2开发板和启明2L1开发板的用户可直接打开配套的“09_Register_MyLib”例程查看该代码,限于篇幅,不在本章中贴出。

代码清单 9-8:hal_entry.c 文件
/* IOPORT模块头文件 (自己写库——构建库函数雏形) */
#include "ioport/ra_ioport.h"

void hal_entry(void)
{
   /* TODO: add your own code here */

   /* 调用取消写保护函数 */
   IOPORT_PinAccessEnable();


   /* 使用 IOPORT 初始化结构体和调用初始化函数来配置PFS寄存器 */
   IOPORT_Init_t led_io_init;
   led_io_init.Port = IO_PORT_04;
   led_io_init.Pin = IO_PIN_00;
   led_io_init.Mode = IO_MODE_GPIO;    //普通GPIO模式,而不是复用功能模式或其他的
   led_io_init.Dir = IO_DIR_OUTPUT;
   led_io_init.OType = IO_OTYPE_PP;
   led_io_init.Drive = IO_DRIVE_LOW;
   led_io_init.Level = IO_LEVEL_HIGH;  //输出高电平(LED熄灭)
   //LED_IO_Init.Pull = IO_NO_PULL; //端口方向处于输出模式下是用不了上拉的,所以这个属性没意义
   IOPORT_Init(&led_io_init); //调用初始化函数,进行 LED1 引脚初始化

   led_io_init.Pin = IO_PIN_03; //更换引脚号
   IOPORT_Init(&led_io_init); //结构体其他属性不变,再次调用初始化函数,进行 LED2 引脚初始化

   led_io_init.Pin = IO_PIN_04; //更换引脚号
   IOPORT_Init(&led_io_init); //结构体其他属性不变,再次调用初始化函数,进行 LED3 引脚初始化


   /** 此时3个LED灯的引脚默认输出的是高电平,所以3个LED灯都会默认不亮
    *  我们先打开所有LED灯,然后在 while 循环里让 LED1 闪烁:每秒钟翻转一次状态
    */
   IOPORT_PinWrite(IO_PORT_04, IO_PIN_00, IO_LEVEL_LOW); //点亮LED1
   IOPORT_PinWrite(IO_PORT_04, IO_PIN_03, IO_LEVEL_LOW); //点亮LED2
   IOPORT_PinWrite(IO_PORT_04, IO_PIN_04, IO_LEVEL_LOW); //点亮LED3

   while(1)
   {
      /* 使用函数 IOPORT_PinToggle 翻转 LED1 引脚电平 */
      IOPORT_PinToggle(IO_PORT_04, IO_PIN_00);
      R_BSP_SoftwareDelay(1000, BSP_DELAY_UNITS_MILLISECONDS);
   }


#if BSP_TZ_SECURE_BUILD
   /* Enter non-secure code */
   R_BSP_NonSecureEnter();
#endif
}

9.3. 下载验证

编写好上述代码,然后将程序编译并下载到开发板之后,按下复位按键来复位开发板, 可以观察到实验现象与上一章的实验现象相同: 开发板上面除了电源指示灯之外的3个 LED 灯当中有两个灯常亮,还有一个灯在缓慢闪烁。 闪烁着的 LED 灯为 LED1,它每秒钟(1000毫秒)便改变一次亮灭的状态。