I.MX6ULL裸机的EPIT实验
0.参考资料
参考手册:正点原子用户手册,IMX6ULL参考手册
其他参考资料:正点原子提供
虚拟机平台:Ubuntu20.04
开发板:正点原子阿尔法开发板。
工程样式:BSP,需要创建:bsp,project,imx6ull,obj,四个文件夹来存放文件。bsp存放的是源码文件,project存放的是主程序和启动文件,obj存放的是编译生成的.o文件,imx6ull存放的是imx6ull的SDK,这个直接用正点原子的,官方的需要修改很麻烦。
1.实验思路分析以及编写步骤
1.1.启动文件
裸机开发需要自己编写启动文件,这一部分理解就行不需要死记硬背,不然会浪费非常多的时间,每次写新的实验回头看一下,每行理解一下就行了。
@ 汇编文件
.global _start
_start:ldr pc, =Reset_Handler /*复位中断服务函数*/ldr pc, =Undefined_Handler /*未定义指令中断服务函数*/ldr pc, =SVC_Handler /*SVC*/ldr pc, =PreAbort_handler /*预取终止*/ldr pc, =DataAbort_Handler /*数据终止*/ldr pc, =NotUsed_Handler /*未使用*/ldr pc, =IRQ_Handler /*IRQ中断*/ldr pc, =FIQ_Handler /*FIQ中断*/
/*复位中断服务函数*/
Reset_Handler:cpsid i /* 关闭IRQ *//* 关闭 I,D Cache和MMU *//* 需要使用到一个叫CP15寄存器*//* 修改SCTLR寄存器,采取读,改,写的方式 */MRC p15, 0, r0, c1, c0, 0 /* 读取SCTLR寄存器的数据到r0寄存器里面 */bic r0, r0, #(1 << 12) /* 关闭 I Cache */bic r0, r0, #(1 << 11) /* 关闭分支预测 */bic r0, r0, #(1 << 2) /* 关闭D Cache */bic r0, r0, #(1 << 1) /* 关闭对齐 */bic r0, r0, #(1 << 0) /* 关闭MMU */ MCR p15, 0, r0, c1, c0, 0 /* 将r0寄存器里面的数据写入到SCTLR里面 */
#if 0/* 设置中断向量偏移 */ldr r0, =0x87800000dsbisbMCR p15, 0, r0, c12, c0, 0 /* 设置VBAR寄存器=0x87800000 */dsbisb
#endif@ 设置处理器进入IRQ模式 mrs r0, cpsr /* 读取cpsr到r0 */bic r0,r0, #0x1f /*清除cpsr的bit4-0*/orr r0, r0, #0x12 /* 使用IRQ模式 */msr cpsr, r0 /* 将r0写入到cpsr */ldr sp, =0x80600000 /* 设置IRQ模式下的sp指针 */@ 设置处理器进入SYS模式 mrs r0, cpsr /* 读取cpsr到r0 */bic r0,r0, #0x1f /*清除cpsr的bit4-0*/orr r0, r0, #0x1f /* 使用SYS模式 */msr cpsr, r0 /* 将r0写入到cpsr */ldr sp, =0x80400000 /* 设置SYS模式下的sp指针 */@ 设置处理器进入SVC模式 mrs r0, cpsr /* 读取cpsr到r0 */bic r0,r0, #0x1f /*清除cpsr的bit4-0*/orr r0, r0, #0x13 /* 使用SVC模式 */msr cpsr, r0 /* 将r0写入到cpsr */ldr sp, =0x80200000 /* 设置SVC模式下的sp指针 */cpsie i /* 打开IRQ */b main /* 跳转到c语言main函数 */
/*未定义指令中断服务函数*/
Undefined_Handler:ldr r0, =Undefined_Handlerbx r0
/*SVC*/
SVC_Handler:ldr r0, =SVC_Handlerbx r0
/*预取终止*/
PreAbort_handler:ldr r0, =PreAbort_handlerbx r0
/*数据终止*/
DataAbort_Handler:ldr r0, =DataAbort_Handlerbx r0
/*未使用*/
NotUsed_Handler:ldr r0, =NotUsed_Handlerbx r0
/*IRQ中断*/
IRQ_Handler:/* 入栈,保护现场*/push {lr} /* 保存lr地址 */push {r0-r3, r12} /* 保存r0-r3,r12寄存器 */mrs r0, spsr /* 读取spsr寄存器 */push {r0} /* 保存spsr寄存器 */mrc p15, 4, r1, c15, c0, 0 /* 从CP15的C0寄存器内的值到R1寄存器中* 参考文档ARM Cortex-A(armV7)编程手册V4.0.pdf P49* Cortex-A7 Technical ReferenceManua.pdf P68 P138*/ add r1, r1, #0X2000 /* GIC基地址加0X2000,也就是GIC的CPU接口端基地址 */ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据* 这个中断号来绝对调用哪个中断服务函数*/push {r0, r1} /* 保存r0,r1 */cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */push {lr} /* 保存SVC模式的lr寄存器 */ldr r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/blx r2 /* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */pop {lr} /* 执行完C语言中断服务函数,lr出栈 */cps #0x12 /* 进入IRQ模式 */pop {r0, r1} str r0, [r1, #0X10] /* 中断执行完成,写EOIR */pop {r0} msr spsr_cxsf, r0 /* 恢复spsr */pop {r0-r3, r12} /* r0-r3,r12出栈 */pop {lr} /* lr出栈 */subs pc, lr, #4 /* 将lr-4赋给pc */
/*FIQ中断*/
FIQ_Handler:ldr r0, =FIQ_Handlerbx r0
2. 选择时钟源与使能时钟
这个之前的时钟实验就有写,但是我还没有把笔记总结出来,今天就一起总结一下。
2.1.时钟树

由图2-1所示,时钟树包含了三个部分,分别是 CLOCK SWITCHER , CLOCK ROOT GENERATOR和SYSTEM CLOCKS,第一部分就是时钟源,第三部分就是我们需要使用到的各个时钟,中间部分则是选择时钟源的各种方式,也就是各种寄存器。
2.2.编写步骤
看我们的标题就知道,我们需要选择时钟源,所以先对第一部分下手,这一部分可以去阅读一下参考手册,内容很多就不一一赘述了,这个开发板的时钟来源于两个部分,而我们只是用到了24Mhz的晶振,另外一个不管,而24M中呢,又有7路PLL,但是这里没有用完,只需要修改主频PLL1以及两个带PFD的也就是PLL2和PLL3,其他PLL没有使用到不用管。

由图可知,我们默认选择主频是996M,然后又被2分频了,就是498M,但是该开发板的时钟频率还是非常强大的只用这样么一点不太够,我们可以手动的进行调频,具体的时钟范围,阅读参考手册时钟源那一章节。
/* ============================================================================== *//* 由图可知,CCM.CCSR寄存器的PLL1_SW_CLK_SEL控制着时钟源的切换 *//* 这一步,先将CPU主频调整为24,让CPU在我们修改主频的时候依然有时钟可用 *//* 先判断CCSR的PLL1_SW_CLK_SEL位是否为0 */if(((CCM->CCSR) >> 2) == 0){/* 是的话就在图中从下往上修改,先将频率选择为24,在修改主频 *//* 先将CCSR的step_sel位切换为0 */CCM->CCSR &= ~(1 << 8);/* 再将CCSR的pll1_sw_clk_sel置为1 */CCM->CCSR |= (1 << 2);}/* 下面就是修改PLL1的主频时钟源了 *//* ((88 << 0) & 0x7f这个是只取7位,防止高位污染 */CCM_ANALOG->PLL_ARM |= (1 << 13) | ((88 << 0) & 0x7f);/* 然后分频调整为2分频 */CCM->CACRR = 1;/* 在将主频从24修改回来 */CCM->CCSR &= ~(1 << 2);/* 以上就可以得到1056/2=528的时钟频率 *//* ============================================================================== */
接下来就是修改PLL2_PFD和PLL3_PFD
/* 创建一个变量用以接受PFD的值 */unsigned int reg = 0;/* 将reg等于PLL2 */reg = CCM_ANALOG->PFD_528;/* 先将源配置清零 */reg &= ~(0x3f3f3f3f);/* 根据官方的每个PFD的频率来赋值,注意官方会虚标 *//* 计算公式 X=528*18/PFDn */reg |= (32 << 24);reg |= (24 << 16);reg |= (16 << 8);reg |= (27 << 0);/* 在将reg赋值给CCM_ANALOG->PFD_528 */CCM_ANALOG->PFD_528 = reg;reg = 0;/* reg又等于480 */reg = CCM_ANALOG->PFD_480;reg &= ~(0x3f3f3f3f);// PFD3_FRACreg |= 19 << 24;reg |= 17 << 16;reg |= 16 << 8;reg |= 12 << 0;CCM_ANALOG->PFD_480 = reg;
上面的步骤将时钟树左边的时钟源就配置完成了,然后就要配置右边的外设时钟。

图2-3是节选了一点不是整张图纸,详细的可以查看imx6ull参考手册的CCM章节。

图2-4就是各个时钟的范围,我们直接拉最大。
/*=================================================================*//* 这里时钟树左边的时钟源就配置完成了,接下来该配置右边的外设时钟了 *//* 这里正常应该配置三个,分别是AXI,AHB和IPG,但是AXI默认是够使用的,所以只需要配置AHB和IPG *//* 查看时钟树,就会发现,AHB和AXI,IPG都有关系,所以先配置AHB是没错的 *//* AHB最大是132 所以得选择PLL2_PFD2=396,分频设置为3分频*//* 先清除原先的数值 */CCM->CBCMR &= ~(3 << 18);CCM->CBCMR |= (1 << 18);/* CBCDR寄存器的bit25置为0 */CCM->CBCDR &= ~(1 << 25);/* 设置3分频 */CCM->CBCDR |= (2 << 10);/* 等待握手完成 */while(CCM->CDHIPR & (1 << 1));/* IPG最大频率为66,即是132/2 所以设置成2分频*/CCM->CBCDR &= ~(3 << 8);CCM->CBCDR |= (1 << 8);//设置 PERCLK_CLK_ROOT 时钟 CSCMR1//PERCLK_CLK_SEL(bit6)为0是ipg,为1是oscCCM->CSCMR1 &= ~(1 << 6);//设置为1分频CCM->CSCMR1 &= ~(7 << 0);
以上,更换时钟源的设置就全部配置结束了。
接下来就是使能时钟,这个可以根据GPIO的需求去使能对应的GPIO时钟,但是为了图方便直接采取全部使能。
void clk_init(void)
{CCM->CCGR0 = 0xFFFFFFFF;CCM->CCGR1 = 0xFFFFFFFF;CCM->CCGR2 = 0xFFFFFFFF;CCM->CCGR3 = 0xFFFFFFFF;CCM->CCGR4 = 0xFFFFFFFF;CCM->CCGR5 = 0xFFFFFFFF;CCM->CCGR6 = 0xFFFFFFFF;
}
这个大家可以看看手册,这个很简单,跟写32的步骤差不多,我之前试过但是写新的GPIO老是把这一步搞忘了,索性全部使能,毕竟CPU的性能还是很强大的,只使用裸机不会造成多大影响。
3.编写中断函数
第一步中的启动文件,有一个IRQ_Handler,这个就是常用的外部中断了,里面有一个跳转到C语言的system_irqhandler, 这个就是中断处理函数。由于启动文件中有这一步,所以我们在做分步测试的时候虽然没有写具体的中断,但是不写这个函数,就编译不过,所以配置了时钟以后就将中断写好。
3.1.定义中断处理函数格式
定义一个中断处理函数的格式,往后的中断函数都依照这个格式写。
typedef void (*system_irq_handler_t)(unsigned int gicciar,void*pagram);
参数1:gicciar就是中断号,中断表中的中断号是多少就是多少。
参数2:pagram传递的参数。
3.2.定义中断处理函数结构体
typedef struct _sys_irq_handle
{system_irq_handler_t irqhandler;void*userParam;
}sys_irq_handle_t;
第一个元素就是中断处理函数,第二个是传递的参数。
3.3.定义中断处理函数表和初始化中断处理函数表
CPU有很多中断,我们将其放置在一个数组中,方便根据下标快速查找。
/* 定义中断处理函数表 */
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS];/* 下面这个就是官方定义的了 */
#define NUMBER_OF_INT_VECTORS 160
/* 一共有160个中断 */
然后我们要将这个表初始化,因为是结构体组成的数组,里面引用了指针,为了避免野指针的风险还是要赋初值,中断处理函数就要先写一个默认的中断处理函数,这个函数里面啥都不干,死循环就行了。
/* 默认中断处理函数 */
void default_irqhandler(unsigned int gicciar,void *userParam)
{while(1){}
}
然后就是写初始化中断处理函数表的函数了
/* 初始化中断处理函数表 */
void system_irqtable_init(void)
{unsigned int i = 0;/* 初始化中断嵌套的值 */irqNesting = 0;for(i = 0; i < NUMBER_OF_INT_VECTORS; i++){/* 赋值为默认中断处理函数 */irqTable[i].irqhandler = default_irqhandler;irqTable[i].userParam = NULL;}
}
注意代码中同样需要初始化一下中断嵌套的值,所以需要先定义一下中断嵌套
/* 中断嵌套 */
static unsigned int irqNesting;
中断嵌套这个变量就是方便CPU的对当前中断嵌套的层级进行追踪,便于系统进行管理以及保护,定义一个中断嵌套计数还是很有必要的。
3.4.中断注册函数
/* 注册中断处理函数 */
void system_register_irqhandler(IRQn_Type irq,system_irq_handler_t handler, void*userParam)
{irqTable[irq].irqhandler = handler;irqTable[irq].userParam = userParam;
}
这个函数就是设置GPIO中断时,需要写的了,他只要是将中断号,和我们自定义的中断函数,以及参数传递进来。
3.5.具体的中断处理函数
/* 具体的中断处理函数*/
void system_irqhandler(unsigned int gicciar)
{uint32_t intNum = gicciar & 0x3ff;if(intNum >= NUMBER_OF_INT_VECTORS){return;}irqNesting++;/* 根据中断ID号,读取中断处理函数,然后执行 */irqTable[intNum].irqhandler(intNum,irqTable[intNum].userParam);irqNesting--;
}
这个就是启动文件中的了,检测到有中断就会调用该函数,不需要手动。
3.6.中断初始化
/* 中断初始化 */
void int_init(void)
{/* GIC初始化 */GIC_Init();system_irqtable_init();/* 中断向量偏移 */__set_VBAR(0x87800000);
}
这个中断向量偏移涉及的东西就比较多了,之后把资料整理处理在详细讲述。
4.gpio函数的编写
- GPIO是输入还是输出,这个使用枚举的方式
- GPIO触发中断的形式(这个也使用枚举的方式)
- 无中断触发,低电平触发,高电平触发,上升沿触发,下降沿触发,上升沿下降沿(双边沿触发)。
- 定义GPIO结构体,有以上两个内容以及默认电平。
- 初始化GPIO。(GDIR)
- 读取GPIO的电平。(PSR/DR)
- 向GPIO写入电平。(DR)
- GPIO使能中断(IMR)
- GPIO禁止中断(IMR)
- 清除中断(ISR)
- 中断初始化函数。(ICR1/ICR2)
按照上面的步骤进行编写代码:
/* 定义枚举,中断触发的模式 */
typedef enum _gpio_interrupt_mode
{kGPIO_NoIntmode = 0U,kGPIO_IntLowLevel = 1U,kGPIO_IntHighLevel = 2U,kGPIO_IntRisingEdge = 3U,kGPIO_IntFallingEdge = 4U,kGPIO_IntRasingFallingEdge = 5U,
}gpio_interrupt_mode_t;
/* 定义GPIO输入或输出 */
typedef enum gpio_direction
{GPIO_INPUT = 0U,GPIO_OUTPUT = 1U,
}gpio_direction_t;
/* 定义gpio结构体 */
typedef struct gpio_pin_config{gpio_direction_t gpio_direction;uint8_t outputLogic;gpio_interrupt_mode_t InterrputMode;
}gpio_pin_config_t;
/* GPIO初始化 */
void gpio_init(GPIO_Type*base,int pin,gpio_pin_config_t*config)
{if(config->gpio_direction == GPIO_INPUT){base->GDIR &= ~(1 << pin);}else{base->GDIR |= (1 << pin);gpio_write_pin(base,pin,config->outputLogic);}/* 初始化中断 */gpio_interrupt_init(base,pin,config->InterrputMode);
}
/* 读取gpio的电平函数 */
int gpio_read_pin(GPIO_Type*base,int pin)
{return 0;
}
/* 写入gpio的电平 */
void gpio_write_pin(GPIO_Type*base,int pin,int value)
{if(value == GPIO_INPUT){base->DR |= (1 << pin);}else{base->DR &= ~(1 << pin);}
}/* gpio使能中断 */
void gpio_enable_interrupt(GPIO_Type*base, int gpio_pin)
{base->IMR |= (1 << gpio_pin);
}/* gpio禁止中断 */
void gpio_disable_interrupt(GPIO_Type*base, int gpio_pin)
{base->IMR &= ~(1 << gpio_pin);
}/* 清除中断 */
void gpio_clear_interrupt(GPIO_Type*base, int gpio_pin)
{base->ISR |= (1 << gpio_pin);
}
/* 初始化中断函数 */
void gpio_interrupt_init(GPIO_Type*base, int gpio_pin,gpio_interrupt_mode_t interrupt_mode)
{volatile uint32_t *icr;int pin = gpio_pin;base->EDGE_SEL &= ~(1U << pin);if(gpio_pin < 16){icr = &(base->ICR1);}else{icr = &(base->ICR2);pin -= 16;}switch(interrupt_mode){case kGPIO_IntLowLevel:*icr &= ~(3 << (2*pin));break;case kGPIO_IntHighLevel:*icr &= ~(3 << (2*pin));*icr |= (1 << (2*pin));break;case kGPIO_IntRisingEdge:*icr &= ~(3 << (2*pin));*icr |= (2 << (2*pin));break;case kGPIO_IntFallingEdge:*icr &= ~(3 << (2*pin));*icr |= (3 << (2*pin));break;case kGPIO_IntRasingFallingEdge:base->EDGE_SEL |= (1 << gpio_pin);break;default:break;}
}
以上就是GPIO库函数的编写了,这个和STM32的库函数还是很像的, 可以看一下参考手册,特别是ICR这个寄存器,他有ICR1和ICR2,没两个IO表示一位pin脚的中断。
5.其他外设代码编写
5.1.led代码编写
这个开发板只有一个led可以使用,如果有多个的话就需要使用枚举的方式表示其他led,所以这里预留
typedef enum led_num
{LED_NUM0 = 0U,LED_NUM1 = 1U,
}led_num_t;
然后就是led初始化和电平跳转了
void led_init(void)
{gpio_pin_config_t led_config;//复用IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03,0);//电气属性IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03,0X10B0);led_config.direction = GPIO_OUTPUT;led_config.outputLogic = 0U;gpio_init(GPIO1,3,&led_config);
}void led_turn(int led_num,int state)
{switch (led_num){case LED_NUM0:if(state == ON){gpio_write_pin(GPIO1,3,1);}else{gpio_write_pin(GPIO1,3,0);}break;default:break;}
}
5.2.蜂鸣器代码编写
void beep_init(void)
{gpio_pin_config_t beep_config;//复用IOMUXC_SetPinMux(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01,0);//电气属性IOMUXC_SetPinConfig(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01,0X10B0);beep_config.gpio_direction = GPIO_OUTPUT;beep_config.outputLogic = 0U;gpio_init(GPIO5,1,&beep_config);
}void beep_turn(int state)
{gpio_write_pin(GPIO5,1,state);
}
5.3.按键代码编写
void key_init(void)
{//定义中断按键的值gpio_pin_config_t key_config;//复用gpioIOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18,0);//配置电气属性IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18,0XF080);key_config.gpio_direction = GPIO_INPUT;key_config.InterrputMode = kGPIO_IntFallingEdge;key_config.outputLogic = 1;gpio_init(GPIO1,18,&key_config);/* ================中断部分==================== */GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);/* 注册中断处理函数 */system_register_irqhandler(GPIO1_Combined_16_31_IRQn,(system_irq_handler_t)gpio1_io18_handler,NULL);/* 使能中断 */gpio_enable_interrupt(GPIO1,18);
}void gpio1_io18_handler (unsigned int gicciar,void *param)
{//蜂鸣器的状态static unsigned char state;delay(10);if(gpio_read_pin(GPIO1,18) == 0) //按键按下了{state = !state;beep_turn(state);}gpio_clear_interrupt(GPIO1, 18); //清除中断标志位}
注意这里加入之前的按键中断实验,但是在中断处理函数里面加入了延时函数(软件),这个不行后面会有更改。
5.4.epit时钟代码编写
void epit_init(unsigned int frac,unsigned int value)
{if(frac > 0XFFF)frac = 0XFFF;EPIT1->CR = 0;EPIT1->CR = (1 << 24 | frac << 4 | 1 << 3 | 1 << 2 | 1 << 1);EPIT1->LR = value;EPIT1->CMPR = 0;GIC_EnableIRQ(EPIT1_IRQn);system_register_irqhandler(EPIT1_IRQn,(system_irq_handler_t)epit_irqhandler,NULL);EPIT1->CR |= 1 << 0;
}
void epit_irqhandler(void)
{static unsigned char state = 0;state = !state;if(EPIT1->SR & (1<<0)){led_turn(LED_NUM0,state);}EPIT1->SR |= 1<<0;
}
epit配置图
可以去手册看一下,epit的详细配置步骤。
6.Makefile编写
CROSS_COMPULE ?= arm-linux-gnueabihf-
TARGET ?= epitCC := $(CROSS_COMPULE)gcc
LD := $(CROSS_COMPULE)ld
OBJCOPY := $(CROSS_COMPULE)objcopy
OBJDUMP := $(CROSS_COMPULE)objdump
# 头文件
INCUDIRS := imx6ull \bsp/clk \bsp/led \bsp/delay \bsp/gpio \bsp/int \bsp/beep \bsp/key \bsp/epit \project
# 源码
SRCDIRS := project \bsp/clk \bsp/led \bsp/gpio \bsp/int \bsp/beep \bsp/key \bsp/epit \bsp/delay
# 给每个头文件前面加上 -I INCLUDE = -I imx6ull -I bsp/clk -I bsp/led -I bsp/delay -I project
INCLUDE := $(patsubst %, -I %, $(INCUDIRS))
# 获取工程的所有.S 文件和 .c 文件
SFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.S))CFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))SFIEDIR := $(notdir $(SFILES))
CFIEDIR := $(notdir $(CFILES))SOBJS := $(patsubst %, obj/%, $(SFIEDIR:.S=.o))
COBJS := $(patsubst %, obj/%, $(CFIEDIR:.c=.o))OBJS := $(SOBJS) $(COBJS)VPATH := $(SRCDIRS)
.PHONY:clean$(TARGET).bin:$(OBJS)$(LD) -Timx6ul.lds -o $(TARGET).elf $^$(OBJCOPY) -O binary -S $(TARGET).elf $@$(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis$(SOBJS) : obj/%.o : %.S$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
$(COBJS) : obj/%.o : %.c$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
clean:rm -rf $(TARGET).elf $(TARGET).bin $(TARGET).dis $(OBJS)
print:@echo INCLUDE = $(INCLUDE)@echo SFILES = $(SFILES)@echo CFILES = $(CFILES)@echo SFIEDIR = $(SFIEDIR)@echo CFIEDIR = $(CFIEDIR)@echo SOBJS = $(SOBJS)@echo COBJS = $(COBJS)@echo OBJS = $(OBJS)