STM32 学习笔记 —— 基于ucgui ucos的LED的PWM亮度控制和ADC实时采样显示程序

写一个ucgui ucos程序,实现ADC通道实时采样并在屏幕显示,以及用滑动条控制LED灯亮度的功能

程序蓝本为奋斗stm32配套例程中的ucgui ucos ADC程序和LED显示控制程序。板子是奋斗最新款的V5.2。

其中ADC程序的功能是实时采样ADC通道10和通道11的电压值,通过屏幕实时显示并通过串口发送;LED闪烁程序的功能是通过滑动条和串口控制LED1,LED2和LED3的闪烁间隔,并显示串口通信的内容。在我们自己的程序里,需要截取原程序的部分功能,并加以修改,最后整合成一个新的程序。但是,和例程不同的地方不仅是要在一个新的程序里实现原来两个程序的功能,对LED的控制也不是调整闪烁的间隔,而是通过定时器输出PWM波控制亮度。这种控制方法可以参考PWM波呼吸灯的例程。由于现有的呼吸灯例程并不是基于ucos系统的,首先我们要对原来的程序进行移植,然后再进行两个程序的功能整合。

现在让我们一步一步开始撸代码之旅吧~(本文略长,请耐心食用)

1.ucos程序移植

首先分析一个ucgui ucos程序各部分的结构。

目录结构:需要修改的地方集中在APP文件夹(存放app.c, Fun.c, demo.h等)FWLib文件夹(存放库文件)和BSP(存放系统初始化文件bsp.c)

1.任务的建立与管理

先分析app.c文件,它实现的是系统的任务创建和任务实现的功能。在文件的开头是一些变量定义和函数声明,由于我们在整合程序的时候没有创建新的任务,因此不需要修改(我采用的是修改任务执行程序的方法,而不是删除后再重新建立任务,这样会简单一些)。在这些变量定义和函数声明之后是main主函数,可以看到主函数中有这样的语句:

/* ucosII 初始化 */
   OSInit();  
                                                
/* 硬件平台初始化 */
   BSP_Init(); 

……

//建立主任务,优先级最高
os_err = OSTaskCreate((void (*) (void *)) App_TaskStart,  //指向任务代码的指针
                          (void *) 0,			//任务开始执行时,传递给任务的参数的指针
               		     (OS_STK *) &App_TaskStartStk[APP_TASK_START_STK_SIZE - 1],	
                         //分配给任务的堆栈栈顶指针
                         (INT8U) APP_TASK_START_PRIO);	//任务优先级
OSTimeSet(0);			 //ucosII 节拍计数器清零
OSStart();               //启动ucos内核

可以看出主函数的功能包括:硬件和软件的初始化,一些全局变量的定义,并且创建了主任务,最后启动ucos内核。

在主函数后面的包括App_TaskStart(void* p_arg)函数和App_TaskCreate(void)函数,前者通过统计任务(在主函数中创建
)建立其他的任务,后者依次对各个子任务进行初始化。以LED闪烁任务为例,一个子任务的初始化包括下面内容:


OSTaskCreateExt(Task_Led1,//指向任务代码的指针
   	(void *)0,//传递给任务的参数的指针
	(OS_STK *)&Task_Led1Stk[Task_Led1_STK_SIZE-1],//任务堆栈栈顶指针
	Task_Led1_PRIO,//任务优先级
	Task_Led1_PRIO,//预备给以后版本的特殊标识符,和当前任务同优先级
	(OS_STK *)&Task_Led1Stk[0],//栈底指针
    Task_Led1_STK_SIZE,//堆栈容量
    (void *)0,//用户附加数据域指针,用于扩展任务控制块
    OS_TASK_OPT_STK_CHK|OS_TASK_OPT_STK_CLR);//选项

对上面的一些名词进行解释:

• 任务堆栈:上下文切换的时候用来保存任务的工作环境,就是STM32的内部寄存器值。

• 任务控制块:任务控制块用来记录任务的各个属性。

• 任务函数:由用户编写的任务处理代码。

我们重点关注的是任务优先级,但是在这次的程序中,任务优先级不需要修改。不过在创建,删除任务以及多任务整合时,需要特别关注优先级的配置,否则常常达不到想要的效果

在ucos系统中,任务优先级的分配方式为0-63,0为最高优先级,64为最低优先级;系统保留了最高和最低4个优先级,因此用户实际可以使用的优先级只有56个

接下来就是各个任务的执行函数,在这个程序中我们要对控制LED的任务Task_Led1进行修改;

在任务执行函数(包括用户界面任务,触屏任务,LED闪烁任务和ADC任务)后面,是一些任务执行中需要调用的函数,比如原先的串口通信任务中需要定义的整型数据转字符串函数和串口格式化输出函数,作为任务的补充部分。整个app.c最后面的部分是一些系统相关的函数,在这个例子中也不需要修改。

总结:app.c文件实现了任务的创建,管理和定义。

2.系统初始化的配置

系统初始化是一个比较繁琐的过程,同时也需要很细心。系统配置任何一个地方出错都会直接导致程序无法呈现预期的效果。一开始我也是因为忘记开时钟导致PWM始终无法输出,无论检查多少次app.c或者修改任务优先级都没有用。stm32里面需要用到的寄存器和各个硬件模块的工作模式都比较复杂,所以在这一步一定要对照stm32参考手册或者例程一步一步修改。事实上在建立一个工程的时候,也应该首先进行系统初始化的配置,再进行任务的创建和界面的设置。

首先,检查一下FWlib里面的库文件是否添加齐全。事实上在工程的文件夹中,这些库文件都是可以找到的;但是在Keil里面,需要把用到的库文件添加到对应的组中,否则编译器会报错。比如,如果用到了定时器,就需要添加stm32f10x_tim.c;如果是ADC,就需要用到stm32f10x_adc.c和stm32f10x_dma.c

在确认库文件添加齐全后,进入bsp.c文件进行系统硬件的初始化。

bsp.c文件的结构包括硬件初始化函数和调用这些函数的一个总初始化函数。硬件初始化函数一般包括下列函数:

void RCC_Configuration(void)    //系统时钟的初始化函数,开启相应的时钟
void GPIO_Configuration(void)   //通用IO口配置函数,确定使能管脚,输出模式等
void ADC_Configuration(void)    //ADC配置函数,包括DMA的配置
void time_ini(void)             //定时器初始化函数,输出PWM波要用到
void NVIC_Configuration(void)   //中断源配置函数,开启中断并配置优先级
void USART_Config(USART_TypeDef* USARTx,u32 baud) //串口配置函数
void OS_CPU_SysTickInit(void)   //ucos系统节拍时钟初始化
void tp_Config(void)            //TFT触摸屏控制初始化,对SPI进行设置
unsigned char SPI_WriteByte(unsigned char data)  //SPI1写函数
void SpiDelay(unsigned int DelayCnt)  //SPI1写延时函数
u16 TPReadX(void)               //触摸屏x轴数据读出函数
u16 TPReadY(void)               //触摸屏y轴数据读出函数

以时钟初始化函数为例,在进行PWM输出的时候一定要开启AFIO时钟复用

void RCC_Configuration(void){

  SystemInit();	 
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);   	//PWM输出一定要开启这一行
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | 
  							RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD| 
  RCC_APB2Periph_GPIOE , ENABLE);
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
  	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
}

实际编写程序的时候,也需要根据要实现不同的功能对这些初始化进行调整,例如,GPIO输出方式的选择:

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;    //用于PWM输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;   //用于普通的LED闪烁

另外,还可以添加更多的系统硬件初始化函数

在这些函数设置好之后,需要调整总的初始化函数

void BSP_Init(void)
{
  
  RCC_Configuration();  	       
  time_ini();        
  NVIC_Configuration();			   
  //GPIO_Configuration();	//本例子中time_ini中包含了GPIO初始化	    
  USART_Config(USART1,115200);	   
  tp_Config();					 	    
  FSMC_LCD_Init();				   
  ADC_Configuration();         
}

3.一些头文件的修改

在APP文件夹中还会存放头文件,对一些系统的变量进行声明和设置,在为程序添加新的功能时,常常也需要声明新的变量,这是就需要找到相应的头文件进行修改

4.编写好看的界面

在这个程序中,ucgui ucos界面编辑的文件是APP文件夹下的Fun.c文件。对于这个文件的结构进行分析,我们可以找到编写界面的一般思路:

和Qt,MFC类似,ucgui ucos的界面管理也可以分为窗口和控件两部分,首先定义对话框资源列表,在列表中指定窗口的名称并添加该窗口内的控件;随后在Fun函数中建立窗口,获得控件的句柄,设置控件,编写while(1)循环内窗口要执行的动作(比如ADC采样功能中每间隔1秒向窗口传输一个数字并显示)最后编写窗口及控件的回调函数(例如slider滑动条的回调函数)

2.实际操作中的代码配置

为了实现预期的功能,在对于原来的例程进行整合的时候,需要对代码进行配置。在通过上面一节大致了解ucgui ucos系统的结构之后,修改程序的思路就比较清晰了。

1.系统硬件初始化的修改

如前所述,第一步是修改系统硬件初始化函数。

开启时钟——这一步非常重要

void RCC_Configuration(void){

  SystemInit();	 
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); 	//PWM输出必须要开启这个时钟
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | 
  							RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD| RCC_APB2Periph_GPIOE , ENABLE);                         //使能GPIO的管脚
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);    //使能DMA时钟,因为ADC转换要用
  	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//使能ADC要用的时钟
}

这里多说两句~

为什么ADC转换要用DMA?

一开始我也觉得两者并没有什么关系。但是在原来的例程中,ADC通道10和通道11都是规则转换,通过查阅数据手册了解到(发现真相了)规则通道转换的值储存在一个仅有的数据寄存器中,所以当转换多个规则通道时需要使用 DMA,这可以避免丢失已经存储在ADC_DR寄存器中的数据。在双 ADC 模式里,为了在主数据寄存器上读取从转换数据,必须使能 DMA 位,即使不使用 DMA 传输规则通道数据 

为啥要开启AFIO的时钟?

这是个巨坑啊啊啊啊啊啊啊啊……以前我从来没想过AFIO寄存器和TIM定时器有啥关系,花了几天的时间调试程序都弄不出来。这里因为PWM输出用到了TIM3的端口复用。在我用的板子中,GPIO的PB5连着一个LED小灯,为了让TIM3定时器输出的波形输出到这个小灯上,需要把TIM3通道2复用到PB5上,即TIM3的部分映像;而这个部分映像又是由复用重映射和调试I/O配置寄存器(AFIO_MAPR) 决定的

位 11:10 

TIM3_REMAP[1:0]:定时器3的重映像 (TIM3 remapping)

这些位可由软件置’1’或置’0’,控制定时器3的通道1至4在GPIO端口的映像。

00:没有重映像(CH1/PA6,CH2/PA7,CH3/PB0,CH4/PB1);

01:未用组合;

10:部分映像(CH1/PB4,CH2/PB5,CH3/PB0,CH4/PB1);

11:完全映像(CH1/PC6,CH2/PC7,CH3/PC8,CH4/PC9)。 注:重映像不影响在PD2上的TIM3_ETR。 

这里我们用到的是部分映像

注意:对寄存器 AFIO_EVCR , AFIO_MAPR 和 AFIO_EXTICRX 进行读写操作前,应当首先打开 AFIO 的时钟

所以因为我们要对AFIO_MAPR寄存器进行读写,如果不开启AFIO的时钟,TIM3根本就不会被复用到PB5上,自然不能看到灯被点亮的效果

定时器和GPIO初始化,这里写到了一个函数里

void time_ini(void){
	TIM_TimeBaseInitTypeDef TIM3_TimeBaseStructure;
	TIM_OCInitTypeDef TIM3_OCInitStructure;
  GPIO_InitTypeDef GPIO_InitStructure;
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;						//PB5复用为TIM3的通道2
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOB, &GPIO_InitStructure);
  /*TIM3局部复用开启,当TIM3局部复用开启时,PB5会被复用为TIM3_CH2*/
  GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3 , ENABLE);			 
  				
   /*-------------------------------------------------------------------
  TIM3CLK=72MHz  预分频系数Prescaler=2 经过分频,定时器时钟为24MHz
  根据公式,通道输出占空比=TIM3_CCR2/(TIM_Period+1),可以得到TIM_Pulse的计数值	 
  捕获-比较寄存器2 TIM3_CCR2= CCR2_Val 	     
  -------------------------------------------------------------------*/
  TIM3_TimeBaseStructure.TIM_Prescaler = 2;						    //预分频器TIM3_PSC=3	 
  TIM3_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;		//计数器向上计数
  TIM3_TimeBaseStructure.TIM_Period =24000;				            //TIM3_APR 频率1KHz 		     
  TIM3_TimeBaseStructure.TIM_ClockDivision = 0x0;					//TIM3_CR1[9:8]=00
  TIM3_TimeBaseStructure.TIM_RepetitionCounter = 0x0;

  TIM_TimeBaseInit(TIM3,&TIM3_TimeBaseStructure);					//写TIM3各寄存器参数
  
  TIM3_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; 			    //PWM模式2
//向上计数时TIMx_CNT

首先对GPIO的引脚进行了复用,然后对TIM3的PWM输出模式进行了设置

ADC初始化

void ADC_Configuration(void)
{
    //首先进行TypeDef
	ADC_InitTypeDef ADC_InitStructure;
	GPIO_InitTypeDef GPIO_InitStructure;
	DMA_InitTypeDef DMA_InitStructure;

    /* Enable DMA clock */
   RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

   /* Enable ADC1 and GPIOC clock */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 , ENABLE);

  	/* DMA channel1 configuration ----------------------------------------------*/
	//使能DMA
	DMA_DeInit(DMA1_Channel1);
	DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address;			            
//DMA通道1的地址
	DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&ADC_ConvertedValue;	            
//DMA传送地址
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;					            
//传送方向
	DMA_InitStructure.DMA_BufferSize = 100;								            
//传送内存大小,100个16位
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;	 
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;				            
//传送内存地址递增
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;		
//ADC转换的数据是16位

	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;				
//传送的目的地址是16位宽度
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;									
//循环
	DMA_InitStructure.DMA_Priority = DMA_Priority_High;
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
    
	/* 允许DMA通道1传输结束中断 */
	DMA_ITConfig(DMA1_Channel1,DMA_IT_TC, ENABLE);


	//使能DMA通道1
	DMA_Cmd(DMA1_Channel1, ENABLE); 
  
    //设置AD模拟输入端口
  	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
  	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
  	GPIO_Init(GPIOC, &GPIO_InitStructure);
	
	//ADC配置
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);                   //设置ADC时钟72MHZ/6=12M 

	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;	//ADC1工作在独立模式
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;		
//模数转换是扫描模式(多通道)还是单次模式(单通道)
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;	
//模数转换是扫描模式(多通道)还是单次模式(单通道)
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
//转换由软件而非外部触发
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//ADC右对齐
	ADC_InitStructure.ADC_NbrOfChannel = 2;//规则通道数目
	ADC_Init(ADC1, &ADC_InitStructure);
	
	/* ADC1 regular channels configuration [¹æÔòģʽͨµÀÅäÖÃ]*/ 

	//ADC1 规则通道配置
  	ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_55Cycles5);
    //通道10采样时间55.5周期	  
  	ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 2, ADC_SampleTime_55Cycles5);	  
  	

	//使能ADC1 DMA 
	ADC_DMACmd(ADC1, ENABLE);
	//使能ADC1
	ADC_Cmd(ADC1, ENABLE);	
	
	//初始化ADC1校准寄存器
	ADC_ResetCalibration(ADC1);
	//检测校准初始化是否完成
	while(ADC_GetResetCalibrationStatus(ADC1));
	
	//开始校准ADC1
	ADC_StartCalibration(ADC1);
	//检测校准是否完成
	while(ADC_GetCalibrationStatus(ADC1));
	
	//ADC1转换启动
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);	 
}

由于使用了ADC和DMA,对NVIC中断源也要进行配置

void NVIC_Configuration(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
  NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;		     //开启DMA1_Channel1中断
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
}

最后修改总的初始化函数

void BSP_Init(void)
{
  
  RCC_Configuration();  	      
  time_ini();
  NVIC_Configuration();			   
  //GPIO_Configuration();			     	  
  USART_Config(USART1,115200);	   
  tp_Config();					       
  FSMC_LCD_Init();				   
  ADC_Configuration();              
}

至此系统硬件初始化完成

2.头文件的修改

在整合两个程序的时候,一些定义的全局变量也要放到一起。因为LED例程用到的变量定义比较多,所以我是把ADC例程中的变量导入到LED例程的头文件中,最后再把LED例程的头文件替换原来ADC例程的头文件

把下面三行放到原来LED头文件的末尾即可

EXT volatile unsigned  short int  ADC_ConvertedValue[100],ADC_ConvertedValue1[2],ADC_TIMEOUT;
EXT unsigned char ADC_STR1[5];	    //ADC1 通道10整数转字符串
EXT unsigned char ADC_STR2[5];		//ADC1 通道11整数转字符串 
EXT OS_EVENT* ADC_SEM;

 

3.任务的修改

任务的修改包括任务建立,优先级修改和任务执行函数的修改,在这里我们只需要修改最后一个。

由于要实现滑动条控制LED亮度的功能,任务执行函数需要修改成LED亮度可调的模式,思路是用屏幕上获取进度条的值控制占空比,通过查阅例程,我们发现milsec1就是进度条1的数值

static  void Task_Led1(void* p_arg)
{
   //这一句一定要加在(void) p_arg;前面,否则会报错
   TIM_OCInitTypeDef  TIM3_OCInitStructure;
   (void) p_arg;	    
   while (1)
   {  
		TIM3_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; 	//PWM模式2
  		TIM3_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//输出禁止      
  		TIM3_OCInitStructure.TIM_Pulse = milsec1*24; //确定占空比,乘的倍数不一定是24
  		TIM3_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; 
  		TIM_OC2Init(TIM3, &TIM3_OCInitStructure);
		OSTimeDlyHMSM(0, 0, 0, 1000);	//一定要有的Delay语句,否则其他任务会被挂起
   }
}

4.界面函数的修改

由于我们最终的程序只需要一个进度条和一个ADC输出的edit控件,首先可以注释掉无关的控件。修改界面需要进入Fun.c文件。

在实际进行操作的时候,发现如果把ADC例程移植到LED例程上,屏幕会黑屏;而反之则不会。所以,在整合两个程序的时候如果出现一些无法解决的问题,可以尝试以不同的程序作为基础。

由于LED程序的控件比较多,我采用的方法是把ADC程序的控件整合到LED程序的窗口内,再把Fun.c文件放回到工程目录中。

这里有一些需要注意的地方:

WM_HWIN text0,text1,text2,text3,text4,edit0,edit1,edit2,slider0,slider1,slider2;
WM_HWIN text5,text6;

新添加的控件,要记得在开头声明,并且不要和已有的控件重名

如果两个窗口要执行的动作不同,循环内的代码也要修改。但是这个循环本身一定要有,因为它里面有刷新屏幕的语句

while (1)
  {
	if(ADC_R==1){                         //1s间隔采样
		ADC_R=0;
		//文本框显示
		TEXT_SetText(text6,ADC_STR1);	  
	}	  
	WM_Exec();							  //屏幕刷新
  }

至此整个工程修改完成~好累啊o(TヘTo)

你可能感兴趣的:(stm32害死人不偿命)