设计思路
设计初心:商家提供的驱动特别难用,我不仅需要加两个DHT11的文件,还要加很多其他文件,看着就头疼,于是自己搞了一个,代码纯手工制作,放心食用,至于一些有关DHT11的数据资料之类的就不在这里放了。
DHT11温湿度传感器属于单线传输方式,因此需要用主机给DHT11模块一个起始信号(18-30ms的低电平与20-40um的高电平),而DHT11返回的信息为40个高电平一共40位分别为:湿度高位,湿度低位,温度高位,温度低位,校验位,校验方式也很简单,湿度高位+湿度低位+温度高位+温度低位 =校验位。
我们需要判断DHT11给主机的返回信号的高电平持续时间,从而知道每一位的高低电平,因此我选择使用输入捕获的方式来得到一组高低电平的时间,使用PWMI模式,获得低电平的持续时间与一个周期的时间,而一个周期的时间-低电平持续时间=高电平持续的时间,我们就可以获得每一位的值。然而,我们想在传输过程中不影响其他程序,并且不被程序中其他中断打断接收信号,还需要一个DMA在每次事件完成后搬运走我们的数据,并且存放到一个足够大的数组中,传输完成后,我们通过这个数组来判断每一位的高低电平,这个过程由于不要求实时,因此并不担心被中断等程序打断。
下面话不多说上代码。
代码讲解
输入捕获配置
void IC_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE);
TIM_InternalClockConfig(TIM1);
TIM_TimeBaseInitTypeDef TIM_TimBaseInitStructure;
TIM_TimBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_TimBaseInitStructure.TIM_Period=65535-1;
TIM_TimBaseInitStructure.TIM_Prescaler=72-1;
TIM_TimBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM1,&TIM_TimBaseInitStructure);
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel=TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter=0x5;
TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Falling;
TIM_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI ;
TIM_ICInit(TIM1,&TIM_ICInitStructure);
TIM_PWMIConfig(TIM1,&TIM_ICInitStructure);
TIM_SelectInputTrigger(TIM1,TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM1,TIM_SlaveMode_Reset);
TIM_Cmd(TIM1,DISABLE);
}
由于我们要接收的数据一个周期在70-120um左右,因此PSC设置为71,即ARR里每增加一个值代表1um,时基单元就选择内部时钟的72M,因为DHT11在收到起始信号后会返回给主机一个80um左右的一个低电平与高电平,程序是在低电平结束后配置这输入捕获通道和DMA,因此DMA会多搬运一个值,这个值我们后面会将它跳过。
至于PWMI模式,前面说过,我们得到的值是一整个周期(下降沿开始,到下降沿结束),因此还需要一个从下降沿上升沿的值,因为每一个周期都要清零CNT,因此我们需要配置一下TRGI的触发源与从模式。
GPIO配置
void GPIO_Output_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_DHT11;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
void GPIO_IPU_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_DHT11;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
GPIO配置无非两种,一个是在给DHT11输出信号时选用的推挽输出,一个是接收信号用的上拉输入,这里不做过多解释
DMA配置
void DHT11_DMA_Init(void)
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&TIM1->CCR1;
DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)oneValue;
DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize=41;
DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;
DMA_InitStructure.DMA_M2M=DMA_M2M_Disable;
DMA_InitStructure.DMA_Priority=DMA_Priority_High;
DMA_Init(DMA1_Channel2,&DMA_InitStructure);
DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&TIM1->CCR2;
DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)twoValue;
DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize=40;
DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;
DMA_InitStructure.DMA_M2M=DMA_M2M_Disable;
DMA_InitStructure.DMA_Priority=DMA_Priority_High;
DMA_Init(DMA1_Channel3,&DMA_InitStructure);
DMA_Cmd(DMA1_Channel2,DISABLE);
DMA_Cmd(DMA1_Channel3,DISABLE);
TIM_DMACmd(TIM1,TIM_DMA_CC1,DISABLE);
TIM_DMACmd(TIM1,TIM_DMA_CC2,DISABLE);
}
DMA在本程序中一共需要两个,如果资源紧张也可以使用一个(DHT11传输的低电平永远为50um左右)。
两个DMA的源地址分别为TIM定时器的CCR1与CCR2的地址,目的地址为两个数组,这两个数组我随手起的名字,oneValue和twoValue两个值哈。上面说到过,由于DHT11会给主机一个低电平和一个高电平的反馈,在高电平结束后就会开始传输数据。但是我们的定时器在ENABLE之后就会开始工作,第一个值(高电平到开始传输数据前的下降沿)还是会给到oneValue,DMA也会搬运这个值,因此我选择将这个多留一位,即DMA1_Channel2为41,当然如果你觉得不美观可以再设置一个while等待这个过程,我主要时考虑其他程序随时可能发生的中断,如果在这个while中程序被中断,那么将接收不到DHT11发送的数据
地址是否自增这个也好理解哈,DMA每次都会搬运同一个源地址(CCR寄存器),但是目的地址是要自增的,数据宽度我选择的HalfWord,由于CCR的值有时会达到120多,防止任何可能的意外造成,因此我的两个数组的数据类型也是无符号16位整型。
这里我要说明一下,上面的定时器和这里的Cmd的参数都是DISABLE,因为这里只需要初始化,并不使用这个(需要先给DHT11发送一个信号)。
启动信号
void StartSignal(void)
{
temint=0;
temdec=0;
humiint=0;
humidec=0;
check=0;
GPIO_Output_Init();
GPIO_ResetBits(GPIOA,GPIO_DHT11);
Delay_ms(20);
GPIO_SetBits(GPIOA,GPIO_DHT11);
Delay_us(30);
GPIO_IPU_Init();
while(GPIO_ReadInputDataBit(GPIOA,GPIO_DHT11)==Bit_RESET);
IC_Start();
}
上面提到过的,要先给DHT11一个起始信号,先设置推挽输出,之后改成上拉输入,启动之前将几个用于存放值的变量清零。
void IC_Start(void)
{
DMA_SetCurrDataCounter(DMA1_Channel2,41);
DMA_SetCurrDataCounter(DMA1_Channel3,40);
DMA_Cmd(DMA1_Channel2,ENABLE);
DMA_Cmd(DMA1_Channel3,ENABLE);
TIM_DMACmd(TIM1,TIM_DMA_CC1,ENABLE);
TIM_DMACmd(TIM1,TIM_DMA_CC2,ENABLE);
TIM_Cmd(TIM1,ENABLE);
}
这里给DMA的传输计数器赋值是为了之后重复使用设计的,在第一次的时候其实并不需要赋值。
接收信号
void ReadSignal(void)
{
while(DMA_GetFlagStatus(DMA1_FLAG_TC2)==RESET);
TIM_Cmd(TIM1,DISABLE);
DMA_Cmd(DMA1_Channel2,DISABLE);
DMA_Cmd(DMA1_Channel3,DISABLE);
TIM_DMACmd(TIM1,TIM_DMA_CC1,DISABLE);
TIM_DMACmd(TIM1,TIM_DMA_CC2,DISABLE);
for(int i=1;i<=8;i++)
{
if(oneValue[i]-twoValue[i-1]>40)
{
humiint|=1;
}
if(oneValue[i+8]-twoValue[i+7]>40)
{
humidec|=1;
}
if(oneValue[i+16]-twoValue[i+15]>40)
{
temint|=1;
}
if(oneValue[i+24]-twoValue[i+23]>40)
{
temdec|=1;
}
if(oneValue[i+32]-twoValue[i+31]>40)
{
check|=1;
}
if(i!=8)
{
temint=temint<<1;
temdec=temdec<<1;
humiint=humiint<<1;
humidec=humidec<<1;
check=check<<1;
}
}
if(temint+temdec+humidec+humiint!=check)
{
temint=99;
temdec=99;
humiint=99;
humidec=99;
check=99;
}
DMA_ClearFlag(DMA1_FLAG_TC2);
}
先保证DMA已经传输完毕,然后失能DMA和TIM,由于oneValue的第一个值不正确(表示DHT11的反馈信号高电平),因此两个括号的 i 要移动一位。最后的99相当于报错,这个可以删掉,可以将上次获取的值存放起来然后赋值给这次的变量
主函数与DHT11.h头文件
测试用代码
#include "stm32f10x.h"
#include "dht11.h"
uint16_t oneValue[41]={0};
uint16_t twoValue[40]={0};
uint8_t temint=0;
uint8_t temdec=0;
uint8_t humiint=0;
uint8_t humidec=0;
uint8_t check=0;
int main()
{
DHT11_Init();
while(1)
{
StartSignal();
ReadSignal();
Delay_ms(1000);
}
}
DHT11_Init()其实就是初始化但不使能输入捕获与DMA,内容如下:
void DHT11_Init(void)
{
IC_Init();
DHT11_DMA_Init();
}
DHT11.h中声明一下main.c中用到的全局变量和数组,以便在DHT11.c的函数中可以使用。
#ifndef __DHT11_H
#define __DHT11_H
#include "stm32f10x.h"
#include "Delay.h"
#define GPIO_DHT11 GPIO_Pin_8 //默认使用PA8引脚
extern uint16_t oneValue[41];
extern uint16_t twoValue[40];
extern uint8_t temint;
extern uint8_t temdec;
extern uint8_t humiint;
extern uint8_t humidec;
extern uint8_t check;
void GPIO_Output_Init(void);
void GPIO_IPU_Init(void);
void IC_Init(void);
void DHT11_DMA_Init(void);
void StartSignal(void);
void ReadSignal(void);
void IC_Start(void);
void DHT11_Init(void);
#endif
以上为用到的变量与函数,delay这个就不在这里放了。
END
如果你发现还有更好的方法写这个驱动,或者你发现我的代码有需要改进的地方,欢迎联系我,咱们可以探讨一下相互学习。