本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id=728461040949
配套资料获取:https://renesas-docs.100ask.net
瑞萨MCU零基础入门系列教程汇总: https://blog.csdn.net/qq_35181236/article/details/132779862
本章目标
瑞萨给开发者提供了“灵活配置软件包”(FSP,Flexible Software Package),从底往上提供了多层次的软件,如下图所示:
从下往上可以分为这几层:
以第 1 个程序为例,工程目录如下:
以工程“MyBlinkyProject”为例,在 hal_entry.c 中,操作 LED 的代码如下:
void hal_entry(void)
{
/* TODO: add your own code here */
extern bsp_leds_t g_bsp_leds;
bsp_leds_t Leds = g_bsp_leds;
while (1)
{
g_ioport.p_api->pinWrite(&g_ioport.p_ctrl, Leds.p_leds[BSP_LED_LED1], BSP_IO_LEVEL_LOW);
R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_SECONDS);
g_ioport.p_api->pinWrite(&g_ioport.p_ctrl, Leds.p_leds[BSP_LED_LED1], BSP_IO_LEVEL_HIGH);
R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_SECONDS);
}
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
使用 FSP 编写程序时有 4 个层次:Application 是用户编写的,Middleware 是第 3 方的代码,BSP 的代码量很少,所以 HAL 层是 FSP 的核心。HAL 层是各个模块的驱动程序,这些驱动程序被称为Module,一个Module向上提供接口供人调用,向下可能要用到其他Module,如下:
怎么使用一个 Module 提供的接口呢?以工程“MyBlinkyProject”为例,有以下 2 种方法调用 r_ioport.c 提供接口:
g_ioport.p_api->pinWrite(&g_ioport.p_ctrl, Leds.p_leds[BSP_LED_LED1], BSP_IO_LEVEL_LOW);
R_IOPORT_PinWrite(&g_ioport_ctrl, Leds.p_leds[BSP_LED_LED1], BSP_IO_LEVEL_LOW);
它们有何不同?这就涉及 FSP 源码设计的理念:
以 GPIO 为例,如下图有 1 个 LED、1 个按键:
对于同一个 MCU,PIN1、PIN2 的操作是类似的,它们的接口函数可以只写一套。但是PIN1 需要被设置为输出功能,PIN2 需要被设置为输入功能并且使能它的内部上拉电阻。即:PIN1、PIN2 的接口函数可以是同一套,但是它们的配置是不一样的。
对于 ioport,使用 ioport_pin_cfg_t 结构体来描述一个引脚的配置:
typedef struct st_ioport_pin_cfg
{
uint32_t pin_cfg; ///< Pin PFS configuration - Use ioport_cfg_options_t parameters
to configure
bsp_io_port_pin_t pin; ///< Pin identifier
} ioport_pin_cfg_t;
比如对于 PIN1,在 FSP 的配置工具里把它配置为输出;对于 PIN2,在 FSP 的配置工具里把它配置为输入、上拉,可以得到下面 2 项:
const ioport_pin_cfg_t g_bsp_pin_cfg_data[] ={
……
{ .pin = BSP_IO_PORT_00_PIN_05,
.pin_cfg = ((uint32_t) IOPORT_CFG_IRQ_ENABLE
| (uint32_t) IOPORT_CFG_PORT_DIRECTION_INPUT
| (uint32_t) IOPORT_CFG_PULLUP_ENABLE) },
{ .pin = BSP_IO_PORT_00_PIN_06,
.pin_cfg = ((uint32_t) IOPORT_CFG_PORT_DIRECTION_OUTPUT
| (uint32_t) IOPORT_CFG_PORT_OUTPUT_LOW) },
……
};
使用硬件前,需要使用接口函数根据用户提供的配置信息来配置硬件。对于 ioport,使用 ioport_api_t 结构体来描述引脚的接口函数,在 r_ioport.c 里可以看到如下结构体:
/* IOPort Implementation of IOPort Driver */
const ioport_api_t g_ioport_on_ioport =
{
.open = R_IOPORT_Open,
.close = R_IOPORT_Close,
.pinsCfg = R_IOPORT_PinsCfg,
.pinCfg = R_IOPORT_PinCfg,
.pinEventInputRead = R_IOPORT_PinEventInputRead,
.pinEventOutputWrite = R_IOPORT_PinEventOutputWrite,
.pinRead = R_IOPORT_PinRead,
.pinWrite = R_IOPORT_PinWrite,
.portDirectionSet = R_IOPORT_PortDirectionSet,
.portEventInputRead = R_IOPORT_PortEventInputRead,
.portEventOutputWrite = R_IOPORT_PortEventOutputWrite,
.portRead = R_IOPORT_PortRead,
.portWrite = R_IOPORT_PortWrite,
};
对于 ioport,配置与接口是分离的:在 ioport_cfg_t 参数里指定引脚、指定配置值,然后调用“pinCfg”函数指针去配置引脚。使用 FSP 的配置工具时,选择某个引脚、设置它的参数,就会生成对应的 ioport_cfg_t 结构体。当我们编写程序调用 r_ioport.c 里的pinCfg”函数指针时,传入这个 ioport_cfg_t 结构体。
假设有如下图所示的两代产品,它们的 LED 接法不一样:
对于第 1 代产品,在 r_ioport.c 里已经实现了如下结构体:
/* IOPort Implementation of IOPort Driver */
const ioport_api_t g_ioport_on_ioport =
{
.open = R_IOPORT_Open,
.close = R_IOPORT_Close,
.pinsCfg = R_IOPORT_PinsCfg,
.pinCfg = R_IOPORT_PinCfg,
.pinEventInputRead = R_IOPORT_PinEventInputRead,
.pinEventOutputWrite = R_IOPORT_PinEventOutputWrite,
.pinRead = R_IOPORT_PinRead,
.pinWrite = R_IOPORT_PinWrite,
.portDirectionSet = R_IOPORT_PortDirectionSet,
.portEventInputRead = R_IOPORT_PortEventInputRead,
.portEventOutputWrite = R_IOPORT_PortEventOutputWrite,
.portRead = R_IOPORT_PortRead,
.portWrite = R_IOPORT_PortWrite,
};
对于第 2 代产品,我们可以在 r_spiioport.c 里实现如下结构体:
/* IOPort Implementation of SPIIOPort Driver */
const ioport_api_t g_spiioport_on_ioport =
{
.open = R_SPIIOPORT_Open,
.close = R_SPIIOPORT_Close,
.pinsCfg = R_SPIIOPORT_PinsCfg,
.pinCfg = R_SPIIOPORT_PinCfg,
.pinEventInputRead = R_SPIIOPORT_PinEventInputRead,
.pinEventOutputWrite = R_SPIIOPORT_PinEventOutputWrite,
.pinRead = R_SPIIOPORT_PinRead,
.pinWrite = R_SPIIOPORT_PinWrite,
.portDirectionSet = R_SPIIOPORT_PortDirectionSet,
.portEventInputRead = R_SPIIOPORT_PortEventInputRead,
.portEventOutputWrite = R_SPIIOPORT_PortEventOutputWrite,
.portRead = R_SPIIOPORT_PortRead,
.portWrite = R_SPIIOPORT_PortWrite,
};
现在有两个接口结构体:g_ioport_on_ioport、g_spiioport_on_ioport,使用哪一个?在哪里指定?需要引入另一个概念:实例。以 ioport 为例,有如下结构体类型:
/** This structure encompasses everything that is needed to use an instance of this
interface.
*/
typedef struct st_ioport_instance
{
ioport_ctrl_t * p_ctrl; ///< Pointer to the control structure for this instance
ioport_cfg_t const * p_cfg; ///< Pointer to the configuration structure for this instance
ioport_api_t const * p_api; ///< Pointer to the API structure for this instance
} ioport_instance_t;
ioport_instance_t
结构体中有 3 个成员:
以工程“MyBlinkyProject”为例,在 ra_gen\common_data.c 中定义了一个实例化对象:
const ioport_instance_t g_ioport =
{ .p_api = &g_ioport_on_ioport, .p_ctrl = &g_ioport_ctrl, .p_cfg = &g_bsp_pin_cfg, };
g_ioport 里:
对于第 1 代产品,g_ioport 的 p_api 指向 g_ioport_on_ioport;对于第 2 代产品,让它指向 g_spiioport_on_ioport。使用实例化结构体 g_ioport 来操作 LED 时,即使更换了底层的操作接口,用户的代码仍然无需改变:
g_ioport.p_api->pinWrite(&g_ioport.p_ctrl, Leds.p_leds[BSP_LED_LED1], BSP_IO_LEVEL_LOW);
如果直接使用接口函数操作 LED 的话,如下:
R_IOPORT_PinWrite(&g_ioport_ctrl, Leds.p_leds[BSP_LED_LED1], BSP_IO_LEVEL_LOW);
对于第 2 代产品,就需要修改成另一个接口,如下:
R_SPIIOPORT_PinWrite(&g_ioport_ctrl, Leds.p_leds[BSP_LED_LED1], BSP_IO_LEVEL_LOW);
使用实例化结构体来操作硬件,在代码的统一性、可读性和可移植性上是有很大优势的。它允许应用程序和硬件之间的进一步抽象。更改底层的外围设备时,只需要修改实例化结构体,不需要更改应用层代码。在实际开发过程中,也可以直接调用底层 API 函数(比如 R_IOPORT_PinWrite),这有两个原因:
一般来说,内部函数遵循“NounNounVerb”(名词名词动词)的命名约定,例如CommunicationAbort()。函数的返回值表示是否成功,函数要对外输出结果时,这些结果只在输出参数中返回,并且第一个参数始终是指向其控制结构体的指针。下面是 FSP 中常用前缀: