文章目录
#六、基于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 GPIO初始化
###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(parameter group) 参数组
该宏定义原型在“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外设寄存器来实现的。
6.3 USART初始化
注:这里为了区分,将BF提供的init()函数改为BF_init()函数,之后Keil程序中将使用BF_init()作为BF源码相关的初始化。
(记)6.3.1 随记
(1)USART估计初始化函数:
259行 printfSerialInit();
527行 uartPinConfigure(serialPinConfig());
540行 serialInit(featureIsEnabled(FEATURE_SOFTSERIAL), SERIAL_PORT_NONE);
(2)PG(parameter group) 参数组
6.3.1 printf()函数初始化与重定义
在BF_init()函数中printf串口通过调用printfSerialInit()函数进行初始化。
而printf()函数的重定义,在不同编译环境下是不同,其在“src/main/common/printf_serial.c”中实现。这里参考AT32官方例程进行了移植。原型如下:
#pragma import(__use_no_semihosting)
struct __FILE
{
int handle;
};
FILE __stdout;
void _sys_exit(int x)
{
x = x;
}
/* __use_no_semihosting was requested, but _ttywrch was */
void _ttywrch(int ch)
{
ch = ch;
}
int fputc(int ch, FILE *f) //重定义printf
{
while(usart_flag_get(PRINT_UART, USART_TDBE_FLAG) == RESET);
usart_data_transmit(PRINT_UART, ch);
return ch;
}
具体原理参考:https://zhuanlan.zhihu.com/p/649806021
6.3.2 uartPinConfigure(serialPinConfig())
这句代码调用了uartPinConfigure()函数,其输入参数是serialPinConfig()执行函数返回的一个变量的指针。无独有偶,serialPinConfig()函数也是使用了之前“6.2.1 statusLedConfig()函数”小节提到PG_DECLARE 宏来定义的。 位于“src/main/drivers/serial.h”中
PG_DECLARE(serialPinConfig_t, serialPinConfig);
(1)PG_DECLARE 宏定义
PG(parameter group) 参数组
该宏定义原型在“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”内联函数关键字。
所以
PG_DECLARE(serialPinConfig_t, serialPinConfig);
展开后的主要内容为:
extern serialPinConfig_t serialPinConfig_System;
static inline const serialPinConfig_t* serialPinConfig(void)
{
return &serialPinConfig_System;
}
即声明serialPinConfig_System变量,并在运行serialPinConfig()函数后返serialPinConfig_System的指针,该指针是serialPinConfig_t类型的。
(2)PG_REGISTER_WITH_RESET_FN宏
又是无独有偶,与“6.2.1 statusLedConfig()函数”小节一样,serialPinConfig_System变量的初始化也依靠于PG_REGISTER_WITH_RESET_FN宏与其所调用的pgResetFn_serialPinConfig函数。引用源码为:
PG_REGISTER_WITH_RESET_FN(serialPinConfig_t, serialPinConfig, PG_SERIAL_PIN_CONFIG, 0);
该宏定义在“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 }) \
/**/
该宏有四个参数,分别为:
- _type (结构体)数据类型
- _name 参数组名字?
- _pgn 参数组序号
- _version 版本号
其中第3行 “extern void pgResetFn_ ## _name(_type *);”为声明参数组复位函数,第4行则又调用了一个宏。程序展开为:
extern void pgResetFn_serialPinConfig(serialPinConfig_t *); \
PG_REGISTER_I(serialPinConfig_t, serialPinConfig, PG_SERIAL_PIN_CONFIG, 0, .reset = {.fn = (pgResetFunc*)&pgResetFn__serialPinConfig}) \
/**/
主要是声明“pgResetTemplate_serialPinConfig”函数,该函数在“src/main/drivers/serial_pinconfig.c”定义。
PG_REGISTER_I宏的原型为:
// Register system config 根据注册表设置系统?
#define PG_REGISTER_I(_type, _name, _pgn, _version, _reset) \
_type _name ## _System; \
_type _name ## _Copy; \
uint32_t _name ## _fnv_hash; \
/* Force external linkage for g++. Catch multi registration */ \
extern const pgRegistry_t _name ## _Registry; \
const pgRegistry_t _name ##_Registry PG_REGISTER_ATTRIBUTES = { \
.pgn = _pgn | (_version << 12), \
.length = 1, \
.size = sizeof(_type) | PGR_SIZE_SYSTEM_FLAG, \
.address = (uint8_t*)&_name ## _System, \
.fnv_hash = &_name ## _fnv_hash, \
.copy = (uint8_t*)&_name ## _Copy, \
.ptr = 0, \
_reset, \
}
该宏有五个参数,分别为:
- _type (结构体)数据类型
- _name 参数组名字?
- _pgn 参数组序号
- _version 版本号
- _reset ?
该宏中又嵌套有PG_REGISTER_ATTRIBUTES宏。
则宏一并展开为
extern void pgResetFn_serialPinConfig(serialPinConfig_t *);
serialPinConfig_t serialPinConfig_System; \
serialPinConfig_t serialPinConfig_Copy; \
uint32_t serialPinConfig_fnv_hash; \
/* Force external linkage for g++. Catch multi registration */ \
extern const pgRegistry_t serialPinConfig_Registry; \
const pgRegistry_t serialPinConfig_Registry PG_REGISTER_ATTRIBUTES = { \
.pgn = PG_SERIAL_PIN_CONFIG | (0 << 12), \
.length = 1, \
.size = sizeof(serialPinConfig_t) | PGR_SIZE_SYSTEM_FLAG, \
.address = (uint8_t*)&serialPinConfig_System, \
.fnv_hash = &serialPinConfig_fnv_hash, \
.copy = (uint8_t*)&serialPinConfig_Copy, \
.ptr = 0, \
.reset = {.fn = (pgResetFunc*)&pgResetFn__serialPinConfig}, \
}
总之宏一个套一个下来,最终实现了如下几个功能:
- 定义三个变量:serialPinConfig_System、serialPinConfig_Copy、serialPinConfig_fnv_hash
- 声明(定义前声明,有点奇怪)、定义一个pgRegistry_t结构体数据类型的变量serialPinConfig_Registry,并进行赋值。
即为.pgn、.reset等子变量赋值。 - 使用之前提到的__attribute__将serialPinConfig_Registry放在.pg_registry段中,屏蔽优化,4字节对齐。
可见这里也只是声明,并示执行pgResetFn__serialPinConfig()函数。
不过经过述宏的学习,本月亮大概猜测出程序是先初始化EPPROM,读取参数并初始化一些变量(包括执行这些函数),然后才去初始化LED、USART等驱动的。果不其然,爷发现在“init.c”的388~392行,初始化了EPPROM,即:
initEEPROM();
ensureEEPROMStructureIsValid();
bool readSuccess = readEEPROM();
点奇怪)、定义一个pgRegistry_t结构体数据类型的变量serialPinConfig_Registry,并进行赋值。
即为.pgn、.reset等子变量赋值。
3. 使用之前提到的__attribute__将serialPinConfig_Registry放在.pg_registry段中,屏蔽优化,4字节对齐。
可见这里也只是声明,并示执行pgResetFn__serialPinConfig()函数。
不过经过述宏的学习,本月亮大概猜测出程序是先初始化EPPROM,读取参数并初始化一些变量(包括执行这些函数),然后才去初始化LED、USART等驱动的。果不其然,爷发现在“init.c”的388~392行,初始化了EPPROM,即:
initEEPROM();
ensureEEPROMStructureIsValid();
bool readSuccess = readEEPROM();
而这也的确是比LED、USART等驱动的初始化要更前的。
这里先在uartPinConfigure(serialPinConfig());前手动加上pgResetFn_serialPinConfig()函数的初始化。
6.3.3 pgResetFn_serialPinConfig()函数
定义在“src/main/drivers/serial_pinconfig.c”中,原型为:
void pgResetFn_serialPinConfig(serialPinConfig_t *serialPinConfig)
{
for (size_t index = 0 ; index < ARRAYLEN(serialDefaultPin) ; index++) {
const serialDefaultPin_t *defpin = &serialDefaultPin[index];
serialPinConfig->ioTagRx[SERIAL_PORT_IDENTIFIER_TO_INDEX(defpin->ident)] = defpin->rxIO;
serialPinConfig->ioTagTx[SERIAL_PORT_IDENTIFIER_TO_INDEX(defpin->ident)] = defpin->txIO;
serialPinConfig->ioTagInverter[SERIAL_PORT_IDENTIFIER_TO_INDEX(defpin->ident)] = defpin->inverterIO;
}
}
(1)serialDefaultPin[]数组
ARRAYLEN是一个判断数组元素个数的宏,在for循环中判断了serialDefaultPin[]数组的元素个数。
serialDefaultPin[]数组是一个serialDefaultPin_t结构体数据类型的数组。serialDefaultPin_t的定义为:
typedef struct serialDefaultPin_s {
serialPortIdentifier_e ident; //串口ID号
ioTag_t rxIO, txIO, inverterIO; //IO标签(uint8)
} serialDefaultPin_t;
数组定义:
static const serialDefaultPin_t serialDefaultPin[] = { //串口默认引脚数组
#ifdef USE_UART1
{ SERIAL_PORT_USART1, IO_TAG(UART1_RX_PIN), IO_TAG(UART1_TX_PIN), IO_TAG(INVERTER_PIN_UART1) },
#endif
……
}
则每个数组元素的标签类变量与“target.h”中的宏定义一一对应。其中“IO_TAG”宏的作用为,加上“DEFIO_TAG__”前缀。 如:
IO_TAG(UART1_RX_PIN)
展开为:
DEFIO_TAG__PA10
再进一步展开
((ioTag_t)((((gpioid) + 1) << 4) | (pin)))
其中gpioid代表寄存器组(A:0 …… D:3 ……),pin为引脚号(1~15)。 替换其中宏参数即得:
((uint8_t)((((0) + 1) << 4) | (10)))
(2)其余部分
其余部分代码的作用就是将serialDefaultPin[]数组中每个元素的“Tag”标签变量赋值给serialPinConfig指针所对应变量(即serialPinConfig_System)子变量数组的每个标签元素。
6.3.4 uartPinConfigure()函数
展开宏,得函数原型为:
void uartPinConfigure(const serialPinConfig_t *pSerialPinConfig)
{
uartDevice_t *uartdev = uartDevice;
for (size_t hindex = 0; hindex < UARTDEV_COUNT; hindex++) {
const uartHardware_t *hardware = &uartHardware[hindex];
const UARTDevice_e device = hardware->device;
uartdev->pinSwap = false;
for (int pindex = 0 ; pindex < UARTHARDWARE_MAX_PINS ; pindex++) {
if (pSerialPinConfig->ioTagRx[device] && (pSerialPinConfig->ioTagRx[device] == hardware->rxPins[pindex].pin)) {
uartdev->rx = hardware->rxPins[pindex];
}
if (pSerialPinConfig->ioTagTx[device] && (pSerialPinConfig->ioTagTx[device] == hardware->txPins[pindex].pin)) {
uartdev->tx = hardware->txPins[pindex];
}
// Check for swapped pins
if (pSerialPinConfig->ioTagTx[device] && (pSerialPinConfig->ioTagTx[device] == hardware->rxPins[pindex].pin)) {
uartdev->tx = hardware->rxPins[pindex];
uartdev->pinSwap = true;
}
if (pSerialPinConfig->ioTagRx[device] && (pSerialPinConfig->ioTagRx[device] == hardware->txPins[pindex].pin)) {
uartdev->rx = hardware->txPins[pindex];
uartdev->pinSwap = true;
}
}
if (uartdev->rx.pin || uartdev->tx.pin) {
uartdev->hardware = hardware;
uartDevmap[device] = uartdev++;
}
}
}