基于路虎开发板的嵌入式实验指导
作者:倾心琴心
联系方式:[email protected]
指导主要有两个部分,一个是官方实验例程指导,一个是嵌入式实验课程实验指导,有错误之处欢迎批评指正。
1、 官方例程指导... 2
1.1、LPC1768简要概述... 2
1.2、路虎开发板概述... 2
1.3、实验例程的讲解与学习... 3
1.3.1 ADC例程... 4
1.3.2 串口例程... 15
2、 嵌入式实验指导... 23
2.1 嵌入式实验总的说明:... 23
2.2 实验工程分析... 24
2.3 实验分析与指导... 31
2.2.1 使用uC/OS-II实现AD采集的功能。... 31
路虎开发板使用的NXP公司的lpc1768芯片,LPC1768 是NXP 公司推出的基于ARM Cortex-M3 内核的微控制器LPC17XX 系列中的一员。LPC17XX 系列Cortex-M3 微处理器用于处理要求高度集成和低功耗的嵌入式应用。LPC17XX 系列微控制器的外设组件包含高达512KB 的flash 存储器、64KB 的数据存储器、以太网MAC、USB 主机/从机/OTG 接口、8 通道DMA 控制器、4 个UART、2 条CAN 通道、2 个SSP 控制器、SPI 接口、3 个IIC 接口、2 输入和2 输出的IIS 接口、8 通道的12 位ADC、10位DAC、电机控制PWM、正交编码器接口、4 个通用定时器、6 输出的通用PWM、带有独立电池供电的超低功耗RTC 和多达70 个的通用IO 管脚。
红色部分就是这款芯片主要提供的功能,在嵌入式项目开发的前期选型时我们就需要在成本和功能之间做一个平衡,选择一款能够满足需要的成本更低的芯片。
下面这张图片是路虎开发板的一些接口介绍,关于路虎开发板的详细信息大家可以查看路虎开发板用户手册(路虎光盘/2.用户手册-User Manual/路虎开发板用户手册.pdf)
这个学期我们将利用这款开发板学习嵌入式开发中一些主要接口的使用方法,例如串口、AD转换、GPIO、LCD、以太网接口、flash编程技术。还有更多的功能大家可以参考例程自己学习理解。
该部分要讲解的实验例程包括路虎开发板所提供的例程(路虎\3.例程-Example\2、基础例程)和NXP官方例程(/lpc17xx.cmsis.driver.library.rar)。后面一个需要从FTP上下载下来,解压后如下所示:
其中core为内核,Drivers为驱动库、Examples为我们需要学习的的例程,makesection是make工具,LPC1700 Peripheral Driver LibraryManual.chm为使用手册(这个可以好好看看,有讲解例程).
下面开始讲解实验例程。
ADC例程包括(3.例程-Example\2、基础例程\【7】路虎_ADC(2012.4.13)\【7】路虎_ADC(2012.4.13)\【实验7】ADC)和(lpc17xx.cmsis.driver.library\Examples\ADC\Polling\Keil)
基础例程是路虎开发板团队自己封装寄存器操作成库,然后使用库完成功能。
我们先看基础例程打开 LandTiger_ADC.uvproj,
项目视图为Group分组,分组管理右键,选择manage components,
在这里可以管理整个项目,ProjectTargets是生成目标,同一个项目可以有不同的项目生成方式,不同的target生成方式不一样,最后获得的bin文件也不一样,Groups是为了更加清晰地管理应用程序文件。Files是所选中的Group所对应的的文件。
我们看到这个项目有三个Group:CMSIS、APP、Read,分别管理内核文件、应用文件、还有就是文档。
首先之首先,我们打开readme.txt
它告诉我们要怎么使用这个例程,
它说使用串口0时,我们需要把JP6 JP7拔掉(JP6 7 是使用串口烧写hex时使用的,ISP下载)
这个程序的主要功能可以打开main.c
我们看到如下一段代码
int main(void)
{
uint32_tADC_Data;
volatile uint32_t ADC_Buf = 0;
uint8_ti;
SystemInit();
UART0_Init();
UART2_Init();
ADC_Init();
while (1)
{
ADC_Data= 0;
for(i = 0;i < 8; i++)
{
ADC_Buf = ADC_Get();
ADC_Data += ADC_Buf;
}
ADC_Data = (ADC_Data / 8); /* 采样8次进行虑波处理 */
ADC_Data = (ADC_Data * 3300)/4096;
UART0_SendString("AD通道5输入电压是:");
UART0_SendChar(ADC_Data); /* 将数据发送到串口显示 */
UART0_SendByte('m');
UART0_SendByte('V');
UART0_SendByte('\n');
UART0_SendByte('\r');
UART2_SendString("路虎(LandTiger)ADC测试程序\n\r");
Delay(1000);
}
}
程序首先调用SystemInit();UART0_Init();UART2_Init();ADC_Init();对相关设备接口进行初始化。SystemInit()完成系统的初始化,它是内核system_17xx.c里面的一个函数,主要是配置时钟还有初始设备接口为初始状态等等,如果不显式调用这个函数,我们也需要SysTick_Config((SystemCoreClock/CLOCK_CONF_SECOND) 这个函数来对系统时钟进行初始化。
在程序主循环中
for(i = 0;i < 8; i++)
{
ADC_Buf = ADC_Get();
ADC_Data +=ADC_Buf;
}
该段代码进行循环采样,采样八次,
ADC_Data =(ADC_Data / 8);/* 采样8次进行虑波处理 */
ADC_Data =(ADC_Data * 3300)/4096;
首先滤波,然后根据公式计算实际电压值
(附录:如何计算实际电压值
1.首先确定ADC用几位表示,最大数值是多少。比如一个8位的ADC,最大值是0xFF,就是255。2.然后确定最大值时对应的参考电压值。一般而言最大值对应3.3V。这个你需要看这个芯片ADC模块的说明。寄存器中有对于输入信号参考电压的设置。3.要计算电压,就把你的ADC数值除以刚才确定的最大数值再乘以参考电压值。比如你ADC值为0x80,那么实际值就是0x80/(0xFF+1)*3.3V = 1.65V4.计算出来的电压值只是ADC管脚处的电压值。你可以用电压表量一下,计算值和实际值是否一样。至于放大器等等,都是芯片外部的事情。外部电路怎么接,和芯片ADC的采样值无关。5.如果你想知道芯片外部某处的电压,你需要从得出的ADC管脚处的电压(比如刚才的1.65V),再根据电路图进行计算
由手册 第29章我们知道 ,LPC1768使用的是12位渐进式AD转换器。所以0xfff是最大值。
所以公式是 ADC_VALUE*3300 / (OXFFF+1)
然后接下来的代码就是使用串口的库从串口输出字符串
UART0_SendString("AD通道5输入电压是:");
UART0_SendChar(ADC_Data); /* 将数据发送到串口显示 */
UART0_SendByte('m');
UART0_SendByte('V');
UART0_SendByte('\n');
UART0_SendByte('\r');
UART2_SendString("路虎(LandTiger)ADC测试程序\n\r");
然后是一个延时,这种利用循环来延时是非常常见的,大家要学会使用,延时多少是根据一个指令占用的时钟周期来计算的。
Delay(1000);
好,基础例程,就到这里,至于uart.c adc.c是怎么实现的,我们不必深究,他们是通过对寄存器,按照一定的时序操作来实现的,涉及到非常多寄存器的操作,感兴趣的同学可以研究一下。
第二个部分是NXP官方例程
这个是我们重点需要学习的例程。
共有五个分组,分别为启动文件、内核文件、驱动、应用、文档。
首先还是打开adc_polling_test.c
int main (void)
{
returnc_entry();
}
c_entry函数的内容如下
PINSEL_CFG_Type PinCfg;
uint32_tadc_value, tmp;
/*Initialize debug via UART0
* ?115200bps
* ?8 data bit
* ?No parity
* ?1 stop bit
* ?No flow control
*/
debug_frmwrk_init();
// printwelcome screen
print_menu();
/*Initialize ADC ----------------------------------------------------*/
/* Becausethe potentiometer on different boards (MCB & IAR) connect
* with different ADC channel, so we have toconfigure correct ADC channel
* on each board respectively.
* If you want to check other ADC channels, youhave to wire this ADC pin directly
* to potentiometer pin (please see schematicdoc for more reference)
*/
#ifdef MCB_LPC_1768
/*
* Init ADC pin connect
* AD0.2 on P0.25
*/
PinCfg.Funcnum= 1;
PinCfg.OpenDrain= 0;
PinCfg.Pinmode= 0;
PinCfg.Pinnum= 25;
PinCfg.Portnum= 0;
PINSEL_ConfigPin(&PinCfg);
#elif defined (IAR_LPC_1768)
/*
* Init ADC pin connect
* AD0.5 on P1.31
*/
PinCfg.Funcnum= 3;
PinCfg.OpenDrain= 0;
PinCfg.Pinmode= 0;
PinCfg.Pinnum= 31;
PinCfg.Portnum= 1;
PINSEL_ConfigPin(&PinCfg);
#endif
/*Configuration for ADC :
* Select: ADC channel 2 (if using MCB1700 board)
* ADC channel 5 (if usingIAR-LPC1768 board)
* ADCconversion rate = 200Khz
*/
ADC_Init(LPC_ADC,200000);
ADC_IntConfig(LPC_ADC,_ADC_INT,DISABLE);
ADC_ChannelCmd(LPC_ADC,_ADC_CHANNEL,ENABLE);
while(1)
{
//Start conversion
ADC_StartCmd(LPC_ADC,ADC_START_NOW);
//Waitconversion complete
while(!(ADC_ChannelGetStatus(LPC_ADC,_ADC_CHANNEL,ADC_DATA_DONE)));
adc_value= ADC_ChannelGetData(LPC_ADC,_ADC_CHANNEL);
//Displaythe result of conversion on the UART0
#ifdef MCB_LPC_1768
_DBG("ADCvalue on channel 2: ");
#elif defined (IAR_LPC_1768)
_DBG("ADCvalue on channel 5: ");
#endif
_DBD32(adc_value);
_DBG_("");
//delay
for(tmp= 0; tmp < 8000000; tmp++);
}
ADC_DeInit(LPC_ADC);
return 1;
这段代码首先是初始化串口调试工具debug_frmwrk_init,跳转到定义发现这个函数就是初始化管脚,里面有一个USED_UART_DEBUG_PORT,需要设定为0,这样就可以使用板子上的UART0与电脑进行通信。
#ifdef MCB_LPC_1768 这个宏是在文件开始的地方定义的。
#defineMCB_LPC_1768
//#defineIAR_LPC_1768
#ifdefMCB_LPC_1768
#define_ADC_INT ADC_ADINTEN2
#define_ADC_CHANNEL ADC_CHANNEL_2
#elifdefined (IAR_LPC_1768)
#define_ADC_INT ADC_ADINTEN5
#define_ADC_CHANNEL ADC_CHANNEL_5
#endif
我们的路虎开发板使用的是AD0.5,所以我们需要定义IAR_LPC_1768宏。
#ifdef MCB_LPC_1768
/*
* Init ADC pin connect
* AD0.2 on P0.25
*/
PinCfg.Funcnum= 1;
PinCfg.OpenDrain= 0;
PinCfg.Pinmode= 0;
PinCfg.Pinnum= 25;
PinCfg.Portnum= 0;
PINSEL_ConfigPin(&PinCfg);
#elif defined (IAR_LPC_1768)
/*
* Init ADC pin connect
* AD0.5 on P1.31
*/
PinCfg.Funcnum= 3;
PinCfg.OpenDrain= 0;
PinCfg.Pinmode= 0;
PinCfg.Pinnum= 31;
PinCfg.Portnum= 1;
PINSEL_ConfigPin(&PinCfg);
#endif
这段代码的逻辑是,如果定义了MCB_LPC_1768宏则初始化管脚0.25为ADC(AD0.2)口,否则如果定义了IAR_LPC_1768则定义管脚1.31为ADC(AD0.5)口.所以我们使用的是0.31 AD0 第五个通道。这个也可以从数据手册中了解到。 我们打开LPC17XX-USER-Manual_0[1].05.pdf
打开chapter8LPC17XX pin connect block
Table 60
在表格Pin name一栏我们找到了p1.31 然后往右看,找到了AD0.5,向上看指导function when 11 ,也就是当功能选择为3(11B)时,这个管脚就是复用为AD转换口0的第5个通道。
所以PinCfg.Funcnum = 3;
OpenDrain代表的是开漏模式 0为非开漏模式
Pinmode是代表管脚模式是上拉、下拉、双边模式
Portnum和Pinnum分别是Port和Pin 早这里就是1和31
PINSEL_ConfigPinc初始化,这个管脚功能就定义好了
/*Configuration for ADC :
* Select: ADC channel 2 (if using MCB1700board)
* ADCchannel 5 (if using IAR-LPC1768 board)
* ADC conversion rate = 200Khz
*/
ADC_Init(LPC_ADC, 200000);
ADC_IntConfig(LPC_ADC,_ADC_INT,DISABLE);
ADC_ChannelCmd(LPC_ADC,_ADC_CHANNEL,ENABLE);
ADC_Init初始化ADC模块,采样频率为20khz
ADC_IntConfig初始化ADC中断,DISABLE为禁止中断
ADC_ChannelCmd使能相应的通道,我们使用的是通道5,所以对通道5进行使能,ENABLE.
while(1)
{
// Start conversion
ADC_StartCmd(LPC_ADC,ADC_START_NOW);
//Wait conversion complete
while(!(ADC_ChannelGetStatus(LPC_ADC,_ADC_CHANNEL,ADC_DATA_DONE)));
adc_value = ADC_ChannelGetData(LPC_ADC,_ADC_CHANNEL);
//Display the result of conversion on theUART0
#ifdefMCB_LPC_1768
_DBG("ADC value on channel 2:");
#elifdefined (IAR_LPC_1768)
_DBG("ADC value on channel 5:");
#endif
_DBD32(adc_value);
_DBG_("");
//delay
for(tmp = 0; tmp < 8000000; tmp++);
}
ADC_DeInit(LPC_ADC);
在这个循环里,ADC_StartCmd启动ADC模块,开始转换,也就是开始采集值,ADC_ChannelGetStatus查看相应通道转换有没有完成,如果完成则跳出while循环,使用ADC_ChannelGetData获得采样转换的值,接下来呢可以使用和路虎例程一样的方法对值进行处理,具体的我就不多说了,给大家一些思考的余地。
串口例程主要包括(3.例程-Example\2、基础例程\【3】路虎_UART(2013.6.3)\【3】路虎_UART(2013.6.3)\UART0_中断接收)和NXP官方例(lpc17xx.cmsis.driver.library\Examples\UART\Interrupt\Keil)
我们主要需要了解的是如何利用串口中断接收数据,而串口轮询(polling)比较简单,大家可以看例程自己理解学习。
首先还是依然看readme.txt
使用串口0需要将JP6 JP7跳线给拔掉。
接下来我们看主程序
第一个部分是串口初始化UART0_Init();
这部分代码是根据直接使用寄存器来完成相关配置,包括配置管脚为串口功能、配置串口参数、使能串口功能、设置串口中断优先级。我们不需要学会对寄存器操作。
接收字符是通过中断来完成的。
在UART.C最后的一个函数
//中断程序
void UART0_IRQHandler()
{
volatileuint32_t iir,count,i;
count = 16;
iir =LPC_UART0->IIR;
switch(iir&0x0F)
{
case0x02: //发送中断
break;
case0x04: //接收中断
//break;
case0x0C: //接收超时中断
if(LPC_UART0->LSR& 0x01) // Rx FIFO is not empty
Rec_date[Rec_len++] = LPC_UART0->RBR;
break;
default:
break;
}
}
UART0_IRQHandler 这个函数是特定的,是在中断向量表中绑定了的。在startup_LPC17xx.s文件中
; External Interrupts
DCD WDT_IRQHandler ; 16: Watchdog Timer
DCD TIMER0_IRQHandler ; 17: Timer0
DCD TIMER1_IRQHandler ; 18: Timer1
DCD TIMER2_IRQHandler ; 19: Timer2
DCD TIMER3_IRQHandler ; 20: Timer3
DCD UART0_IRQHandler ; 21: UART0
DCD UART1_IRQHandler ; 22: UART1
DCD UART2_IRQHandler ; 23: UART2
DCD UART3_IRQHandler ; 24: UART3
DCD PWM1_IRQHandler ; 25: PWM1
DCD I2C0_IRQHandler ; 26: I2C0
DCD I2C1_IRQHandler ; 27: I2C1
//************
我们主要来分析这个中断函数的内容:
获得IIR,这个是获得中断标志,通过中断标志知道发生中断的是发送、接收还是接收超时中断。
如果是接收中断,我们可以将接收缓冲区的内容LPC_UART0->RBR保存到程序的数据区,以便后续处理。
我们依然还是从函数入口开始分析。
c_entry()前面部分内容(349-422)
// UART Configuration structure variable
UART_CFG_TypeUARTConfigStruct;
// UART FIFOconfiguration Struct variable
UART_FIFO_CFG_TypeUARTFIFOConfigStruct;
// Pinconfiguration for UART0
PINSEL_CFG_TypePinCfg;
uint32_t idx,len;
__IO FlagStatusexitflag;
uint8_tbuffer[10];
/*
* Initialize UART0 pin connect
*/
PinCfg.Funcnum= 1;
PinCfg.OpenDrain= 0;
PinCfg.Pinmode= 0;
PinCfg.Pinnum =2;
PinCfg.Portnum= 0;
PINSEL_ConfigPin(&PinCfg);
PinCfg.Pinnum =3;
PINSEL_ConfigPin(&PinCfg);
/* InitializeUART Configuration parameter structure to default state:
* Baudrate = 9600bps
* 8 data bit
* 1 Stop bit
* None parity
*/
UART_ConfigStructInit(&UARTConfigStruct);
UARTConfigStruct.Baud_rate= 57600;
// InitializeUART0 peripheral with given to corresponding parameter
UART_Init((LPC_UART_TypeDef*)LPC_UART0, &UARTConfigStruct);
/* InitializeFIFOConfigStruct to default state:
* -FIFO_DMAMode = DISABLE
* -FIFO_Level = UART_FIFO_TRGLEV0
* -FIFO_ResetRxBuf = ENABLE
* -FIFO_ResetTxBuf = ENABLE
* -FIFO_State = ENABLE
*/
UART_FIFOConfigStructInit(&UARTFIFOConfigStruct);
// InitializeFIFO for UART0 peripheral
UART_FIFOConfig((LPC_UART_TypeDef*)LPC_UART0, &UARTFIFOConfigStruct);
// Enable UARTTransmit
UART_TxCmd((LPC_UART_TypeDef*)LPC_UART0, ENABLE);
/* EnableUART Rx interrupt */
UART_IntConfig((LPC_UART_TypeDef*)LPC_UART0, UART_INTCFG_RBR, ENABLE);
/* Enable UARTline status interrupt */
UART_IntConfig((LPC_UART_TypeDef*)LPC_UART0, UART_INTCFG_RLS, ENABLE);
/*
* Do not enable transmit interrupt here, sinceit is handled by
* UART_Send() function, just to reset TxInterrupt state for the
* first time
*/
TxIntStat =RESET;
// Reset ringbuf head and tail idx
__BUF_RESET(rb.rx_head);
__BUF_RESET(rb.rx_tail);
__BUF_RESET(rb.tx_head);
__BUF_RESET(rb.tx_tail);
/* preemption= 1, sub-priority = 1 */
NVIC_SetPriority(UART0_IRQn, ((0x01<<3)|0x01));
/* EnableInterrupt for UART0 channel */
NVIC_EnableIRQ(UART0_IRQn);
这部分代码的操作顺序和功能路虎的例程中UART0_Init()是一样的。
首先是PINSEL_ConfigPin(&PinCfg);初始化芯片管脚功能。
然后使用UART_ConfigStructInit(&UARTConfigStruct);对结构体UARTConfigStructInit进行初始化,初始化后该结构体就设置了一些默认的参数,使用UARTConfigStructInit可以设置一些串口的参数,包括波特率,停止位,奇偶校验等等。然后用该结构体使用UART_Init((LPC_UART_TypeDef*)LPC_UART0, &UARTConfigStruct);对串口0的参数进行设置。
接下来是FIFO的操作。FIFO是一个先入先出的双口缓冲器,它主要是对连续的数据流进行缓存,防止在数据操作时丢失数据。
UART_FIFOConfigStructInit(&UARTFIFOConfigStruct);//初始化结构体UARTFIFOConfigStruct
// InitializeFIFO for UART0 peripheral
UART_FIFOConfig((LPC_UART_TypeDef*)LPC_UART0, &UARTFIFOConfigStruct);//使用该结构体对LPC_UART0参数进行设置。。
最重要的参数是FIFO_Level 它可以设置多少个字符发生一次中断。
// 使能串口发送
UART_TxCmd((LPC_UART_TypeDef *)LPC_UART0,ENABLE);
/* 使能串口接收中断 */
UART_IntConfig((LPC_UART_TypeDef *)LPC_UART0,UART_INTCFG_RBR, ENABLE);
/*使能串口数据中断状态 */
UART_IntConfig((LPC_UART_TypeDef *)LPC_UART0,UART_INTCFG_RLS, ENABLE);
/* 第一优先级 = 1,次优先级 = 1 */
NVIC_SetPriority(UART0_IRQn,((0x01<<3)|0x01));
/* 使能串口0中断*/
NVIC_EnableIRQ(UART0_IRQn);
以上这段代码流程在使用串口对串口进行初始化的时候基本是一致的。
在这个例程中我们主要需要了解的就是串口中断。
void UART0_IRQHandler(void)
{
uint32_tintsrc, tmp, tmp1;
/* 决定中断源*/
intsrc =UART_GetIntId(LPC_UART0);
tmp = intsrc& UART_IIR_INTID_MASK;
// 接收状态
if (tmp ==UART_IIR_INTID_RLS){
//检查状态
tmp1 =UART_GetLineStatus(LPC_UART0);
//除去接收就绪和发送等待状态
tmp1 &=(UART_LSR_OE | UART_LSR_PE | UART_LSR_FE \
|UART_LSR_BI | UART_LSR_RXFE);
// 如果有错误
if (tmp1) {
UART_IntErr(tmp1);
}
}
// 如果接收到数据或者是字符接收超时
if ((tmp ==UART_IIR_INTID_RDA) || (tmp == UART_IIR_INTID_CTI)){
UART_IntReceive();
}
// 数据发送中断。
if (tmp ==UART_IIR_INTID_THRE){
UART_IntTransmit();
}
}
这段代码的作用就是根据中断类型来进行处理,我们主要需要做的就是处理接收中断,一般情况下我们不会用到发送中断。
接下来看我在一个项目中用到的中断处理
void _UART_IRQHander(void)
{
uint32_tintsrc, tmp, tmp1;
//OSIntEnter();
// Determinethe interrupt source
intsrc =UART_GetIntId((LPC_UART_TypeDef *)_LPC_UART);
tmp = intsrc& UART_IIR_INTID_MASK;
// Receive LineStatus
if (tmp ==UART_IIR_INTID_RLS){
// Checkline status
tmp1 =UART_GetLineStatus((LPC_UART_TypeDef *)_LPC_UART);
// Mask outthe Receive Ready and Transmit Holding empty status
tmp1 &=(UART_LSR_OE | UART_LSR_PE | UART_LSR_FE \
|UART_LSR_BI | UART_LSR_RXFE);
// If anyerror exist
if (tmp1) {
UART_IntErr(tmp1);
}
}
// Receive DataAvailable or Character time-out
if ((tmp ==UART_IIR_INTID_RDA) || (tmp == UART_IIR_INTID_CTI)){
UART_IntReceive();
//UART_Receive((LPC_UART_TypeDef*)_LPC_UART,&tmpchar,1,BLOCKING);
}
// TransmitHolding Empty
if (tmp ==UART_IIR_INTID_THRE){
//UART_IntTransmit();
}
//OSIntExit();
}
//发生接收中断时调用,基本上是发生8字节中断后调用,或者超时后调用
void UART_IntReceive(void)
{
uint8_t rlen;//接收数据的长度
uint8_tchars[16];
uint8_t index =0;
//接收一个字符
rlen =UART_Receive((LPC_UART_TypeDef *)_LPC_UART,chars,16,NONE_BLOCKING);
for(index=0;index
{
my_printf("-%d-",chars[index]);
UART_Send((LPC_UART_TypeDef*)_LPC_UART,chars,index,BLOCKING);
}
}
主要关心的是UART_IntReceive();
这段代码主要是中断接收数据,然后通过调试串口打印出来,同时将字符回发给接收的串口。(这里需要注意的是,我们的调试串口和通信串口不能设置为同一个串口,这样会干扰我们对数据的分析)
在实验三种我们就可以利用这段代码,比如在会发的时候同时将采样的值回发回去。
嵌入式原理课程主要是让大家了解嵌入式的整个情况,而实验课呢主要是侧重让大家学会使用uc/os-II来完成我们嵌入式的开发任务。uC/OS-II是一个非常优秀的嵌入式操作系统,它移植非常方便,可方便的剪裁,甚至在非常小的51单片机上也能够运行。使用uC/OS-II主要是使用它的多任务的功能,简化编程任务。uC/OS-II代码非常简洁清晰,大家有空可以去看看它的源码。后面有时间呢我也会给大家讲讲。
为了更好的讲解代码规范等等一些东西,我就直接使用实验三的参考代码来讲解。
这个工程有11个分组,分别为内核、启动文件、用户任务程序、用户程序库、文档、uC/OS-II移植文件、uC/OS-II源码、uC/OS-II配置文件、UIP移植文件、UIP源码、NXP LPC1768驱动库。这么多的分组和文件大家可能刚看到会有点头晕。但其实非常简单,当初这样建立分组呢,就是为了非常清晰地管理代码文件。我们主要使用到的分组就是User和UserLib这两个。
其中User下面的AppTaskDisplay.c就是一个任务。
void AppTaskDisplay(void * pdata)
{
INT8U err;
pdata = pdata;
my_printf("display");
while(1)
{
OSSemPend(semSample,0,&err);//等待数据
OSSemPend(semDataVisit,0,&err);//互斥访问
dislpay(sampleData);
OSSemPost(semDataVisit);
OSTimeDly(200);
}
}
一个任务主要就是一个任务函数,在创建任务的使用我们需要将这个指针传递给OSTaskCreate函数。
一个任务函数一般里面有一个主循环,这个循环一般是死循环,就是说这个任务需要一直进行下去,任务里最后一行代码一般会是一个延时函数,一个任务需要主动让出时间给其他任务执行,否则优先级低的任务将永远没有调度运行的机会。
这个是刚才那个.C文件所对应的的头文件 这里使用了一个头文件保护,避免函数声明重复包含。使用头文件的目的,就是让这个函数能够在需要使用的地方能够#include “xxx.h”就能够把头文件里面的内容包含进去,这样头文件里面的内容对于那个文件就是可见的,从而可以调用这里面的函数,以及访问数据等等。这个是个一个好习惯,有助于大家讲各个模块封装起来,重复使用。
下面讲解如何创建任务:
OSTaskCreate(AppTaskDisplay,
(void*)0,
&App_TaskDisplayStk[APP_TASK_DISPLAY_STK_SIZE-1],
OS_TASK_DISPLAY_PRIO);
这个函数就是用于创建任务的,函数的不同颜色部分代表不一样的参数类型,AppTaskDisplay就是一个函数名称,(void*)0就是空指针,相当于NULL,App_TaskDisplayStk是堆栈,其实就是一个数组,APP_TASK_DISPLAY_STK_SIZE是一个宏,定义堆栈的大小,OS_TASK_DISPLAY_PRIO是一个宏,定义的是该任务的优先级。
设计多任务程序时,首先要确定需要多少个任务,在实验三中,我们只需要两个任务就可以了, 一个采集数据,一个显示,当然我们还有很多不同的方法。确定好多少个任务之后,需要确定任务的优先级,优先级高的任务有更高的调度权,比如当两个任务都是就绪的,那么优先级高的任务将获得CPU并运行,而当优先级低的任务在运行时如果优先级高的任务就绪,则优先级低的任务将或被剥夺执行的权利,而需要将CPU让渡给优先级高的任务。再然后就需要确定堆栈大小,堆栈大小跟数据操作有关系,如果在某个任务中使用了较大的数组,则需要大的堆栈,以便容纳数据。
我们点开APP.C文件。发现它包含有很多.h文件
打开app_cfg.h文件
这里就是定义任务优先级和堆栈大小
一般大的3个优先级要给系统使用,所以我们使用4以后的优先级(数值越大优先级越低)。
打开main.c文件
显示如下
首先是包含各个头文件。其中includes.h文件是基础库的包含,也就是标准工程会包含的库头文件。
接下来就可以包含自己的库,比如LEDControl.h 、KeyControl.h等等
在头文件包含的后面是堆栈声明。可以看到堆栈是一个类型为OS_STK的数组,大小就是我们在APP_CFG.H文件里宏所定义的大小,记得加上注释,这样代码会更加清晰
在启动任务中,此处创建任务区域就可以嗲用OSTaskCreate创建任务
因为我们这次实验要求按键以后开始采样,所以我在这边使用了一个死循环,如果P2.11没有按键事件则一直循环,如果有按键则进入if模块,创建采样任务和显示任务。
任务创建好以后,uC/OS会继续执行,因为AppTaskStart任务优先级很高,在循环外面OSTaskDel(OS_PRIO_SELF);//当所有任务创建完成删除本任务
这条语句将自身这个任务删除,我们创建的任务就可以调度运行了,可以确定的是首先运行的APP_TASK_SAMPLE,因为它的优先级比较高。
接下来我来演示一下创建一个任务。
设计一个任务主要包括定义任务函数、任务堆栈数组、任务优先级、任务堆栈大小。
这里主要的功能还是非常简单的。我主要讲两个地方
按键后对应的管脚会发生电平变化。
比如这段代码,我们读取port这个端口,它返回的是这个端口对应的一个32位的值,比如Port=2时,返回的就是p2.0-p2.31的值组成的一个32位指,然后我们需要判断对应的位上是不是1或者是0,借此来判断是否有按键事件。在路虎开发板中,如果有按键对应的管脚p2.11应该是会是0.
为什么我加上了一个DelayMs(10)呢?
这个函数内容如下
其实就是延时。
这个原因是按键抖动,因为我们的按钮是机械按钮,所以会发生抖动,具体情况大家可以去百度一下咯。
采样任务采集到的数据我们使用全局变量保存起来,访问使用互斥信号量控制,保证数据的完整性和可靠性。而显示任务也通过信号量来了解是否已经采样到了数据,这个大家可以去看看操作系统的读者写者问题,如果有不懂之处再问我,给大家一些思考的地方。