六、基于FreeRTOS的BF移植到Keil
(记)6.1随记
6.1.1 C语言中的#与##
参考:https://www.jb51.net/article/282832.htm
在C语言中#与##是两个常用的预处理运算符。分别代表 记号串化(#)、记号黏结(##),其常用于宏定义中的形参字符串操作。如
记号串化(#):
记号串化可以将函数式宏定义中的实参转换为字符串。在函数式宏定义中,如果替换列表中有“#”,则其后的预处理记号必须是当前宏的形参。在预处理期间,“#”连同它后面的形参一起被实参取代。例如
#include <stdio.h>
#define PSQR(x) printf("The square of " #x " is %d.\n",((x)*(x)))
int main(void)
{
int y = 5;
PSQR(y);
PSQR(2 + 4);
PSQR( 3 * 2 );
return 0;
}
程序运行结果如下:
第1次调用宏时,用"y"替换#x。第2次调用宏时,用"2 + 4"替换#x。第3次调用宏时,用"3 * 2"替换#x。
ANSI C字符串的串联特性将这些字符串与printf()语句的其他字符串组合,生成最终的字符串。例如,第1次调用变成:
printf(“The square of " “y” " is %d.\n”,((y)*(y)));
然后,字符串串联功能将这3个相邻的字符串组合成一个字符串:
“The square of y is %d.\n”
如果传入的实参中间有空白,则不管有多少,都被转换为一个空格,参数开头和末尾的空白都被删除。例如第3次调用宏时,实参“3 * 2 ”转换为“3 * 2”。
记号黏结(##)
与#运算符类似,##运算符可用于函数式宏的替换部分,它把两个记号组合成一个记号。例如,可以这样定义函数式宏:
#define XNAME(n) x ## n
然后,展开宏XNAME(4)为x4。
记号黏结的作用是将几个预处理记号合并为一个。在一个函数式宏定义中,如果一个预处理记号的前面或者后面有"##",则该记号将与它前面或者后面的记号合并,如果该预处理记号是宏的形参,则用实参执行合并。例如:
#define F(x, y, z) x##y##r
char F(a, b, c);
第2行的宏调用,其扩展之后如下:
char abr;
6.1 LED(GPIO)——IO初始化
###6.1.1 IOInitGlobal()函数
该函数在初始化init()中初调用,定义在“src/main/drivers/io.c”中284行,函数原型为:
void IOInitGlobal(void)
{
ioRec_t *ioRec = ioRecs;
for (unsigned port = 0; port < ARRAYLEN(ioDefUsedMask); port++) { //循环扫描引脚组(port: 端口)
for (unsigned pin = 0; pin < sizeof(ioDefUsedMask[0]) * 8; pin++) { //循环扫描每组各引脚
if (ioDefUsedMask[port] & (1 << pin)) { //如果位设置为1
ioRec->gpio = (gpio_type *)(GPIOA_BASE + (port << 10)); //“<< 10”代表*0x400,为各端口(A~E)偏移地址
ioRec->pin = 1 << pin;
ioRec++;
}
}
}
}
(1) ioRec_t结构体数据类型
函数头使用该结构体体数据类型定义了变量。
Rec全称record,记录?。该数据类型主要包含了GPIO属性相关信息,定义在“src/main/drivers/io_impl.h” 32行处,原型为:
typedef struct ioRec_s {
gpio_type *gpio; //GPIO所有相关寄存器
uint16_t pin; //引脚号(0~15)
resourceOwner_e owner; //所属驱动,LED、Gyro等
uint8_t index; //引脚自定义序号
} ioRec_t;
(2) ioDefUsedMask[]数组
ARRAYLEN是一个判断数组元素个数的宏定义。在for循环中使用了ARRAYLEN来判断ioDefUsedMask[]数组的元素个数。
ioDefUsedMask[]数组原型为:
static const uint16_t ioDefUsedMask[DEFIO_PORT_USED_COUNT] = { DEFIO_PORT_USED_LIST }; //IO使用遮掩数组,位置1表使用,0表不使用
ioDefUsedMask[]数组是一个“IO使用遮掩数组”,“DEFIO_PORT_USED_COUNT”的宏定义值为5,而DEFIO_PORT_USED_LIST宏定义为:
# define DEFIO_PORT_USED_LIST DEFIO_PORT_A_USED_MASK,DmEFIO_PORT_B_USED_MASK,DEFIO_PORT_C_USED_MASK,DEFIO_PORT_D_USED_MASK,DEFIO_PORT_E_USED_MASK
这是一个“引脚底遮掩列表”,其有5个元素,对应A~E五组IO引脚,也是赋给ioDefUsedMask[]数组各元素的值,该数组是uint16_t 类型的,每个元素的每个位都对应一个引脚,置1表示使用,置0表示遮掩而不使用。
该值最终在 中定义:
#define TARGET_IO_PORTA 0xffff //目标使用的端口?只用低16位?1表使用?
#define TARGET_IO_PORTB 0xffff
#define TARGET_IO_PORTC 0xffff
#define TARGET_IO_PORTD 0xffff
#define TARGET_IO_PORTE 0xffff
可以看到,默认使用所有引脚。
6.2 LED(GPIO)——LED初始化
inty中LED初始化的函数为:
ledInit(statusLedConfig());
其中statusLedConfig()为一个函数指针。
6.2.1 statusLedConfig()函数
该函数在“1_keil/src/main/drivers/light_led.h”声明:
typedef struct statusLedConfig_s {
ioTag_t ioTags[STATUS_LED_NUMBER]; //Tag:标签
uint8_t inversion; //电平反转
} statusLedConfig_t;
PG_DECLARE(statusLedConfig_t, statusLedConfig); //声明对应函数指针等
(1)statusLedConfig_t LED状态设置结构体数据类型
与LED设置相关的结构体数据类型,STATUS_LED_NUMBER为使用的LED数量(Github源程序设置为3)。ioTag_t数据类型实际定义为uint8_t,而ioTags[]数组元素的作用为储存对应LED标签,之后程序会根据这个标签查询到对应的IO寄存器等?
####(2)PG_DECLARE PG声明宏定义
其中“PG”的含义暂时还不太清楚,猜测应该与存在硬盘中参数的设置有关。
该宏定义原型在“src/main/pg/pg.h”中:
// Declare system config
#define PG_DECLARE(_type, _name) \
extern _type _name ## _System; \
extern _type _name ## _Copy; \
static inline const _type* _name(void) { return &_name ## _System; }\
static inline _type* _name ## Mutable(void) { return &_name ## _System; }\
struct _dummy \
/**/
其主要作用是根据输入的_type数据类型与_name名称,声明一些变量,定义一些函数指针。这之间运用到了之前章节讲到的“##”记号黏结预处理运算符和“inline”内联函数关键字。
综上所述,statusLedConfig()函数的定义如下:
static inline const statusLedConfig_t* statusLedConfig(void)
{
return &statusLedConfig_System;
}
即运行statusLedConfig()函数后返回一个statusLedConfig_System指针,该指针是statusLedConfig_t类型的
(3)statusLedConfig_System变量的定义与初始化
但是statusLedConfig()函数只是返回这样一个指针,却并未对statusLedConfig_System变量其进行其它操作,其还有其他定义与初始化的地方。
在源码中,statusLedConfig_System变量在“src/main/drivers/light_led.c”中定义:
PG_REGISTER_WITH_RESET_FN(statusLedConfig_t, statusLedConfig, PG_STATUS_LED_CONFIG, 0);
PG_REGISTER_WITH_RESET_FN宏在“src/main/pg/pg.h”中定义,原型为:
#define PG_REGISTER_WITH_RESET_FN(_type, _name, _pgn, _version) \
extern void pgResetFn_ ## _name(_type *); \
PG_REGISTER_I(_type, _name, _pgn, _version, .reset = {.fn = (pgResetFunc*)&pgResetFn_ ## _name }) \
/**/
而宏PG_REGISTER_I的定义中涉及许多汇编代码段部分,暂时还未搞清楚具体原理。
猜测statusLedConfig_System变量在pgResetFn_statusLedConfig()函数中初始化,不知道是不是编译环境原因,这个函数在程序进行时并未被执行,这里先手动将它加入init()函数中。
pgResetFn_statusLedConfig()函数原型为:
void pgResetFn_statusLedConfig(statusLedConfig_t *statusLedConfig)
{
statusLedConfig->ioTags[0] = IO_TAG(LED0_PIN);
statusLedConfig->ioTags[1] = IO_TAG(LED1_PIN);
statusLedConfig->ioTags[2] = IO_TAG(LED2_PIN);
statusLedConfig->inversion = 0
#ifdef LED0_INVERTED
| BIT(0)
#endif
#ifdef LED1_INVERTED
| BIT(1)
#endif
#ifdef LED2_INVERTED
| BIT(2)
#endif
;
}
6.2.2 ledInit函数
定义在“src/main/drivers/light_led.c”中,函数原型为:
void ledInit(const statusLedConfig_t *statusLedConfig)
{
ledInversion = statusLedConfig->inversion; //电平反转标志
for (int i = 0; i < STATUS_LED_NUMBER; i++) {
if (statusLedConfig->ioTags[i]) {
leds[i] = IOGetByTag(statusLedConfig->ioTags[i]);
IOInit(leds[i], OWNER_LED, RESOURCE_INDEX(i));
IOConfigGPIO(leds[i], IOCFG_OUT_PP);
} else {
leds[i] = IO_NONE;
}
}
LED0_OFF;
LED1_OFF;
LED2_OFF;
}
(1)IO_TAG(LED1_PIN)
这段代码运用了许多宏定义。
首先LED0_PIN等都在“src/main/target/AT32F437DEV/target.h”中定义,该文件是用来总体设置每个引脚功能与连接设备的。LED1_PIN定义如下:
#define LED1_PIN PD14 //confirm on LQFP64
其次IO_TAG宏的作用是运用“##”字符黏结加入“DEFIO_TAG__”前缀(好几层调用啊,DEF:def,定义; TAG:标签)。所以IO_TAG(LED1_PIN)展开后为:
DEFIO_TAG__PD14
这又是一个宏了,经过若干调用展开为:
((((gpioid) + 1) << 4) | (pin))
其中gpioid代表寄存器组(A:0 …… D:3 ……),pin为引脚号(1~15)。DEFIO_TAG__PD14对应的就是:
((((3) + 1) << 4) | (13)) = 77
(2) IOGetByTag(statusLedConfig->ioTags[i])
for循环中的这段代码,根据之前定义的IO标签,从好久之前(6.1小节)中所提到的ioRecs[]数组中找到对应的元素,并反回其指针。ioRecs[]有80个元素(对应5*16个引脚),如若是定义为PD14引脚,则返回 “&ioRecs[ 4 * 16 + 14]”
(3)IOInit()函数
这个函数只是用来设置输入ioRec_t类型变量指针的值,而没有真正初始化GPIO
(4)IOConfigGPIO()函数
这个函数老师真正GPIO外设的初始化函数,输入ioRec_t类型变量指针与IO电气参数。
而这个函数主要调用了RCC_ClockCmd()函数用来使能GPIO外设时钟,调用了gpio_init()函数用来初始化GPIO其它参数。其中gpio_init()函数是AT32库中的一个函数。
(5)LED外设宏操作
对LED操作的宏,以LED1为例来说,有LED1_TOGGLE、LED1_OFF、LED1_ON分别代表LED电平反转、关、开。对LED的GPIO的操作主要是通过对“6.3.7 GPIO设置/清除寄存器(GPIOx_SCR)”、“6.3.6 GPIO输出数据寄存器(GPIOx_ODT)”两个GPIO外设寄存器来实现的。
o_init()函数用来初始化GPIO其它参数。其中gpio_init()函数是AT32库中的一个函数。
####(5)LED外设宏操作
对LED操作的宏,以LED1为例来说,有LED1_TOGGLE、LED1_OFF、LED1_ON分别代表LED电平反转、关、开。对LED的GPIO的操作主要是通过对“6.3.7 GPIO设置/清除寄存器(GPIOx_SCR)”、“6.3.6 GPIO输出数据寄存器(GPIOx_ODT)”两个GPIO外设寄存器来实现的。