前言
学习UCOSIII也已经有俩周了,跟着原子哥的教程以及官方的手册把所有的知识点大概都看了一遍,但是纸上得来终觉浅,作为嵌入式的学习者,实战是最最重要的环节,所以今天花了大概俩个小时左右做了一个小的实战项目,也用到了不少的知识,也感觉对知识起到了不错的巩固作用。这篇博客就记录一下这个小项目,也记录一下在做的过程中遇到的一些问题。
项目需求
1、使用DHT11读取温湿度值
2、RTC时钟
3、按键 key0切换背景颜色 key1打开/关闭蜂鸣器 key2打开/关闭流水灯
4、显示日期、时间、温湿度
项目分析
按键扫描以及各个按键对应任务的同步可以使用信号量的方式来同步
流水灯的运行、RTC时间获取、DHT11数据的读取可以使用软件定时器的方式来实现
需要的任务有:按键任务,LED任务,蜂鸣器任务,LCD显示任务
硬件连接
DHT11的数据引脚连接到PA6
别的就没有什么硬件连接了。。。
软件设计
DHT11的驱动代码(有重要的点)
dht11.h
#ifndef __DHT11_H
#define __DHT11_H
#include "sys.h"
//切换IO输入输出
#define DHT11_IO_IN() {GPIOA->MODER&=~(1<<12);GPIOA->MODER|=0<<12;}
#define DHT11_IO_OUT() {GPIOA->MODER&=~(1<<12);GPIOA->MODER|=1<<12;}
//输出或读取
#define DHT11_DQ_IN PAin(6)
#define DHT11_DQ_OUT PAout(6)
u8 DHT11_Init(void); //Init DHT11
u8 DHT11_Read_Data1(u8 *temperature,u8 *humidity); //Read DHT11 Value
u8 DHT11_Read_Byte(void); //Read One Byte
u8 DHT11_Read_Bit(void); //Read One Bit
u8 DHT11_Check(void); //Chack DHT11
void DHT11_Rst(void); //Reset DHT11
#endif
dht11.c
#include "dht11.h"
#include "delay.h"
#include "stdio.h"
#include "usart.h"
//Reset DHT11
void DHT11_Rst(void)
{
u16 i;
DHT11_IO_OUT(); //SET OUTPUT
DHT11_DQ_OUT=0; //GPIOA.0=0
//delay_ms(20); //Pull down Least 18ms
for(i=0;i<20000;i++)
{
delay_us(1);
}
DHT11_DQ_OUT=1; //GPIOA.0=1
delay_us(30); //Pull up 20~40us
}
u8 DHT11_Check(void)
{
u8 retry=0;
DHT11_IO_IN();//SET INPUT
while (DHT11_DQ_IN&&retry<100)//DHT11 Pull down 40~80us
{
retry++;
delay_us(1);
};
if(retry>=100)
return 1;
else
retry=0;
while (!DHT11_DQ_IN&&retry<100)//DHT11 Pull up 40~80us
{
retry++;
delay_us(1);
};
if(retry>=100)
return 1;//chack error
return 0;
}
u8 DHT11_Read_Bit(void)
{
u8 retry=0;
while(DHT11_DQ_IN&&retry<100)//wait become Low level
{
retry++;
delay_us(1);
}
retry=0;
while(!DHT11_DQ_IN&&retry<100)//wait become High level
{
retry++;
delay_us(1);
}
delay_us(40);//wait 40us
if(DHT11_DQ_IN)
return 1;
else
return 0;
}
u8 DHT11_Read_Byte(void)
{
u8 i,dat;
dat=0;
for (i=0;i<8;i++)
{
dat<<=1;
dat|=DHT11_Read_Bit();
}
return dat;
}
u8 DHT11_Read_Data1(u8 *temperature,u8 *humidity)
{
u8 buf[5];
u8 i;
DHT11_Rst();
if(DHT11_Check()==0)
{
for(i=0;i<5;i++)
{
buf[i]=DHT11_Read_Byte();
}
if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4])
{
*humidity=buf[0];
*temperature=buf[2];
}
}
else
return 1;
return 0;
}
u8 DHT11_Init(void)//DHT11初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);//使能GPIOA时钟
//PA6 推挽推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;//LED0和LED1对应IO口
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN;//上拉
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIO
DHT11_Rst(); //复位
return DHT11_Check();
}
注意:在void DHT11_Rst(void) 中因为使用UCOSIII改变了一个语句的写法,就是以下的
//delay_ms(20); //Pull down Least 18ms
for(i=0;i<20000;i++)
{
delay_us(1);
}
delay_ms(20);是我在裸机程序中的写法,但是移植到UCOS的程序中后却出现读出的数据一直是0的情况,在网上翻阅资料后发现DHT11是单线通信的模块,时序对它来说至关重要,而原子哥的delay_ms();d的函数是这样写的
//延时nms
//nms:要延时的ms数
//nms:0~65535
void delay_ms(u16 nms)
{
if(delay_osrunning&&delay_osintnesting==0)//如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度)
{
if(nms>=fac_ms) //延时的时间大于OS的最少时间周期
{
delay_ostimedly(nms/fac_ms); //OS延时
}
nms%=fac_ms; //OS已经无法提供这么小的延时了,采用普通方式延时
}
delay_us((u32)(nms*1000)); //普通方式延时
}
我们可以看出,如果OS已经在运行并且不是在中断中时,而且延时的ms数大于OS的最少时间周期(我设置的是5ms) 会使用操作系统的延时函数,也就意味着会就行任务调度,而这样的结果对对于DHT11这样的单线模块来说基本就GG了。。所以说我这里将它改成使用delay_us();函数
void delay_us(u32 nus)
{
u32 ticks;
u32 told,tnow,tcnt=0;
u32 reload=SysTick->LOAD; //LOAD的值
ticks=nus*fac_us; //需要的节拍数
delay_osschedlock(); //阻止OS调度,防止打断us延时
told=SysTick->VAL; //刚进入时的计数器值
while(1)
{
tnow=SysTick->VAL;
if(tnow!=told)
{
if(tnow<told)tcnt+=told-tnow; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else tcnt+=reload-tnow+told;
told=tnow;
if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
};
delay_osschedunlock(); //恢复OS调度
}
可以看到delay_us();函数是会将调度器加锁的(通过调用delay_osschedlock();),这样就不会发生任务调度打断延时的情况。
主函数
1、首先是一些头文件的包含,以及一些关于操作系统的任务、定时器、信号量的定义。
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "includes.h"
#include "os_app_hooks.h"
#include "lcd.h"
#include "dht11.h"
#include "key.h"
#include "rtc.h"
#include "beep.h"
//任务优先级
#define START_TASK_PRIO 3
//任务堆栈大小
#define START_STK_SIZE 512
//任务控制块
OS_TCB StartTaskTCB;
//任务堆栈
CPU_STK START_TASK_STK[START_STK_SIZE];
//任务函数
void start_task(void *p_arg);
/*按键扫描任务*/
//任务优先级
#define KEY_TASK_PRIO 4
//任务堆栈大小
#define KEY_STK_SIZE 128
//任务控制块
OS_TCB KeyTaskTCB;
//任务堆栈
CPU_STK KEY_TASK_STK[KEY_STK_SIZE];
void key_task(void *p_arg);
/*开关蜂鸣器读取任务*/
//任务优先级
#define BEEP_TASK_PRIO 5
//任务堆栈大小
#define BEEP_STK_SIZE 128
//任务控制块
OS_TCB BeepTaskTCB;
//任务堆栈
CPU_STK BEEP_TASK_STK[BEEP_STK_SIZE];
void beep_task(void *p_arg);
/*LCD显示任务*/
//任务优先级
#define SHOW_TASK_PRIO 6
//任务堆栈大小
#define SHOW_STK_SIZE 128
//任务控制块
OS_TCB ShowTaskTCB;
//任务堆栈
CPU_STK SHOW_TASK_STK[SHOW_STK_SIZE];
//任务函数
void show_task(void *p_arg);
/*LED闪烁任务*/
//任务优先级
#define LED_TASK_PRIO 7
//任务堆栈大小
#define LED_STK_SIZE 128
//任务控制块
OS_TCB LedTaskTCB;
//任务堆栈
CPU_STK LED_TASK_STK[LED_STK_SIZE];
//任务函数
void led_task(void *p_arg);
////////////////////////////////////////
/*定时器1--采集温湿度数据*/
OS_TMR tmr1;
void tmr1_callback(void *p_tmr, void *p_arg);
/*定时器2--定时刷新显示数据*/
OS_TMR tmr2;
void tmr2_callback(void *p_tmr,void *p_arg);
/*定时器3--跑马灯*/
OS_TMR tmr3;
void tmr3_callback(void *p_tmr,void *p_arg);
//按键信号量
OS_SEM BACKGROUND_SEM; //背景切换
OS_SEM BEEP_SEM; //开关蜂鸣器
OS_SEM LED_SEM; //开关LED
2、接下来是几个全局变量的定义
u8 tem,hum; //温度和湿度数据
u8 timebuf[40]; //存放时间数据
RTC_TimeTypeDef RTC_TimeStruct; //RTC时间结构体
RTC_DateTypeDef RTC_DateStruct; //RTC日期结构体
//LCD刷屏时使用的颜色
int lcd_discolor[14]={ WHITE, RED, BLUE, BRED,
GRED, GBLUE, BLACK, MAGENTA,
GREEN, CYAN, YELLOW,BROWN,
BRRED, GRAY };
3、main函数,包括系统初始化,LCD的一些初始显示,以及开始任务的创建
int main(void)
{
OS_ERR err;
CPU_SR_ALLOC();
delay_init(168); //时钟初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断分组配置
uart_init(115200); //串口初始化
LED_Init(); //LED初始化
LCD_Init(); //LCD初始化
KEY_Init(); //按键初始化
DHT11_Init(); //DHT11初始化
My_RTC_Init(); //RTC初始化
BEEP_Init(); //蜂鸣器初始化
RTC_Set_WakeUp(RTC_WakeUpClock_CK_SPRE_16bits,0); //配置WAKE UP中断,1秒钟中断一次
POINT_COLOR=RED;
LCD_ShowString(30,10,200,16,16,"Explorer STM32F4");
LCD_ShowString(30,30,200,16,16,"UCOSIII Practice");
LCD_ShowString(30,50,200,16,16,"KEY0:change background");
LCD_ShowString(30,70,200,16,16,"KEY1:OPEN/OFF the BEEP");
LCD_ShowString(30,90,200,16,16,"KEY2:OPEN/OFF the LED");
POINT_COLOR=BLUE;
OSInit(&err); //初始化UCOSIII
OS_CRITICAL_ENTER();//进入临界区
//创建开始任务
OSTaskCreate((OS_TCB * )&StartTaskTCB, //任务控制块
(CPU_CHAR * )"start task", //任务名字
(OS_TASK_PTR )start_task, //任务函数
(void * )0, //传递给任务函数的参数
(OS_PRIO )START_TASK_PRIO, //任务优先级
(CPU_STK * )&START_TASK_STK[0], //任务堆栈基地址
(CPU_STK_SIZE)START_STK_SIZE/10, //任务堆栈深度限位
(CPU_STK_SIZE)START_STK_SIZE, //任务堆栈大小
(OS_MSG_QTY )0, //任务内部消息队列能够接收的最大消息数目,为0时禁止接收消息
(OS_TICK )0, //当使能时间片轮转时的时间片长度,为0时为默认长度,
(void * )0, //用户补充的存储区
(OS_OPT )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR, //任务选项
(OS_ERR * )&err); //存放该函数错误时的返回值
OS_CRITICAL_EXIT(); //退出临界区
OSStart(&err); //开启UCOSIII
while(1);
}
4、开始任务函数,其中包括信号量的创建、软件定时器的创建、任务函数的创建
//开始任务函数
void start_task(void *p_arg)
{
OS_ERR err;
CPU_SR_ALLOC();
p_arg = p_arg;
CPU_Init();
#if OS_CFG_STAT_TASK_EN > 0u
OSStatTaskCPUUsageInit(&err); //统计任务
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN //如果使能了测量中断关闭时间
CPU_IntDisMeasMaxCurReset();
#endif
#if OS_CFG_SCHED_ROUND_ROBIN_EN //当使用时间片轮转的时候
//使能时间片轮转调度功能,时间片长度为1个系统时钟节拍,既1*5=5ms
OSSchedRoundRobinCfg(DEF_ENABLED,1,&err);
#endif
//创建定时器1
OSTmrCreate((OS_TMR *)&tmr1, //定时器1
(CPU_CHAR *)"tmr1", //定时器名字
(OS_TICK )0, //不延时
(OS_TICK )100, //100*10=1000ms
(OS_OPT )OS_OPT_TMR_PERIODIC, //周期模式
(OS_TMR_CALLBACK_PTR)tmr1_callback,//定时器1回调函数
(void *)0, //参数为0
(OS_ERR *)&err); //返回的错误码
//创建定时器2
OSTmrCreate((OS_TMR *)&tmr2, //定时器1
(CPU_CHAR *)"tmr2", //定时器名字
(OS_TICK )0, //不延时
(OS_TICK )20, //20*10=200ms
(OS_OPT )OS_OPT_TMR_PERIODIC, //周期模式
(OS_TMR_CALLBACK_PTR)tmr2_callback,//定时器1回调函数
(void *)0, //参数为0
(OS_ERR *)&err); //返回的错误码
//创建定时器3
OSTmrCreate((OS_TMR *)&tmr3, //定时器1
(CPU_CHAR *)"tmr3", //定时器名字
(OS_TICK )0, //不延时
(OS_TICK )50, //50*10=500ms
(OS_OPT )OS_OPT_TMR_PERIODIC, //周期模式
(OS_TMR_CALLBACK_PTR)tmr3_callback,//定时器1回调函数
(void *)0, //参数为0
(OS_ERR *)&err); //返回的错误码
OS_CRITICAL_ENTER(); //进入临界区
//创建背景切换信号量
OSSemCreate((OS_SEM* ) &BACKGROUND_SEM,
(CPU_CHAR* )"background_sem",
(OS_SEM_CTR)0,
(OS_ERR* )&err);
//创建蜂鸣器开关信号量
OSSemCreate((OS_SEM* ) &BEEP_SEM,
(CPU_CHAR* )"beep_sem",
(OS_SEM_CTR)0,
(OS_ERR* )&err);
//创建流水灯开关信号量
OSSemCreate((OS_SEM* ) &LED_SEM,
(CPU_CHAR* )"led_sem",
(OS_SEM_CTR)0,
(OS_ERR* )&err);
//创建按键扫描任务
OSTaskCreate((OS_TCB * )&KeyTaskTCB,
(CPU_CHAR * )"KEY task",
(OS_TASK_PTR )key_task,
(void * )0,
(OS_PRIO )KEY_TASK_PRIO,
(CPU_STK * )&KEY_TASK_STK[0],
(CPU_STK_SIZE)KEY_STK_SIZE/10,
(CPU_STK_SIZE)KEY_STK_SIZE,
(OS_MSG_QTY )0,
(OS_TICK )0,
(void * )0,
(OS_OPT )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR,
(OS_ERR * )&err);
//创建蜂鸣器开关任务
OSTaskCreate((OS_TCB * )&BeepTaskTCB,
(CPU_CHAR * )"BEEP task",
(OS_TASK_PTR )beep_task,
(void * )0,
(OS_PRIO )BEEP_TASK_PRIO,
(CPU_STK * )&BEEP_TASK_STK[0],
(CPU_STK_SIZE)BEEP_STK_SIZE/10,
(CPU_STK_SIZE)BEEP_STK_SIZE,
(OS_MSG_QTY )0,
(OS_TICK )0,
(void * )0,
(OS_OPT )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR,
(OS_ERR * )&err);
//创建LCD显示任务
OSTaskCreate((OS_TCB * )&ShowTaskTCB,
(CPU_CHAR * )"SHOW task",
(OS_TASK_PTR )show_task,
(void * )0,
(OS_PRIO )SHOW_TASK_PRIO,
(CPU_STK * )&SHOW_TASK_STK[0],
(CPU_STK_SIZE)SHOW_STK_SIZE/10,
(CPU_STK_SIZE)SHOW_STK_SIZE,
(OS_MSG_QTY )0,
(OS_TICK )0,
(void * )0,
(OS_OPT )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR,
(OS_ERR * )&err);
//创建LED闪烁任务
OSTaskCreate((OS_TCB * )&LedTaskTCB,
(CPU_CHAR * )"LED task",
(OS_TASK_PTR )led_task,
(void * )0,
(OS_PRIO )LED_TASK_PRIO,
(CPU_STK * )&LED_TASK_STK[0],
(CPU_STK_SIZE)LED_STK_SIZE/10,
(CPU_STK_SIZE)LED_STK_SIZE,
(OS_MSG_QTY )0,
(OS_TICK )0,
(void * )0,
(OS_OPT )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR,
(OS_ERR * )&err);
OSTmrStart(&tmr1,&err);
OSTmrStart(&tmr2,&err);
OS_TaskSuspend((OS_TCB*)&StartTaskTCB,&err); //挂起开始任务
OS_CRITICAL_EXIT(); //退出临界区
}
5、其余的任务函数以及软件定时器的回调函数
//按键扫描任务函数
void key_task(void *p_arg)
{
u8 key;
OS_ERR err;
while(1)
{
key = KEY_Scan(0);
switch(key)
{
case KEY0_PRES: //当key0按下的话打开发送切换背景信号量
OSSemPost(&BACKGROUND_SEM,OS_OPT_POST_1,&err);
printf("切换背景1\r\n");
break;
case KEY1_PRES: //当key1按下的话发送开关蜂鸣器定时器2
OSSemPost(&BEEP_SEM,OS_OPT_POST_1,&err);
printf("开/关蜂鸣器\r\n");
break;
case KEY2_PRES: //当key2按下的话发送开关流水灯的信号量
OSSemPost(&LED_SEM,OS_OPT_POST_1,&err);
printf("开/关流水灯\r\n");
break;
}
OSTimeDlyHMSM(0,0,0,10,OS_OPT_TIME_PERIODIC,&err); //延时10ms
}
}
//蜂鸣器任务函数
void beep_task(void *p_arg)
{
u8 num;
OS_ERR err;
while(1)
{
OSSemPend(&BEEP_SEM,0,OS_OPT_PEND_BLOCKING,0,&err); //请求信号量
printf("BEEP\r\n");
BEEP=!BEEP; //打开/关闭蜂鸣器
num++;
OSTimeDlyHMSM(0,0,0,5,OS_OPT_TIME_PERIODIC,&err); //延时5ms
}
}
//LED任务函数
void led_task(void *p_arg)
{
u8 status=0; //流水灯的运行状态,默认为关闭
OS_ERR err;
while(1)
{
OSSemPend(&LED_SEM,0,OS_OPT_PEND_BLOCKING,0,&err); //请求信号量
if(status==0) //打开
{
//打开流水灯
status=1;
OSTmrStart(&tmr3,&err); //开启定时器
LED0=0;
LED1=1;
}
else if(status==1) //关闭
{
//关闭流水灯
status=0;
OSTmrStop(&tmr3,OS_OPT_TMR_NONE,0,&err); //关闭定时器
LED0=1;
LED1=1;
}
OSTimeDlyHMSM(0,0,0,5,OS_OPT_TIME_PERIODIC,&err); //延时5ms
}
}
//显示任务(切换背景)
void show_task(void *p_arg)
{
OS_ERR err;
u8 num;
while(1)
{
OSSemPend(&BACKGROUND_SEM,0,OS_OPT_PEND_BLOCKING,0,&err); //请求任务量
num++;
LCD_Fill(6,121,233,313,lcd_discolor[num%14]); //刷屏
OSTimeDlyHMSM(0,0,1,0,OS_OPT_TIME_PERIODIC,&err); //延时1s
}
}
//定时器1的回调函数-----读取数据
void tmr1_callback(void *p_tmr, void *p_arg)
{
static u16 tmr1_num=0;
CPU_SR_ALLOC();
OS_CRITICAL_ENTER(); //进入临界区
DHT11_Read_Data1(&tem,&hum);
OS_CRITICAL_EXIT(); //退出临界区
tmr1_num++;
}
//定时器2的回调函数-----显示数据
void tmr2_callback(void *p_tmr, void *p_arg)
{
RTC_GetTime(RTC_Format_BIN,&RTC_TimeStruct); //获取时间
//把数字转为字符
sprintf((char*)timebuf,"Time:%02d:%02d:%02d",RTC_TimeStruct.RTC_Hours,RTC_TimeStruct.RTC_Minutes,RTC_TimeStruct.RTC_Seconds);
//LCD显示
LCD_ShowString(30,140,210,16,24,timebuf);
RTC_GetDate(RTC_Format_BIN, &RTC_DateStruct); //获取日期
//把数字转为字符
sprintf((char*)timebuf,"Date:20%02d-%02d-%02d",RTC_DateStruct.RTC_Year,RTC_DateStruct.RTC_Month,RTC_DateStruct.RTC_Date);
//LCD显示
LCD_ShowString(30,170,210,16,24,timebuf);
sprintf((char*)timebuf,"Week:%d",RTC_DateStruct.RTC_WeekDay);
//LCD显示星期信息
LCD_ShowString(30,200,210,16,24,timebuf);
//显示温湿度
LCD_ShowString(30,230,210,16,24,"tem: C");
LCD_ShowString(130,230,210,16,24,"hum: %");
LCD_ShowNum(78,230,tem,2,24);
LCD_ShowNum(178,230,hum,2,24);
}
//定时器3的回调函数-----流水灯
void tmr3_callback(void *p_tmr,void *p_arg)
{
LED0=!LED0;
LED1=!LED1;
}
实验效果
复位后
切换背景后
打开LED后(LED0和LED1交替闪烁,见谅只有图片没有闪烁效果-.-)
总结
这个小实验还是比较简单的,但是还是在编写的过程中练习了UCOS的API的使用,对于DHT11的延时问题的解决对以后模块代码的编写也留下了重要的经验,视屏就不发了,工程我也发出来,链接: link.