写作目的
本人小白一个,之前驱动传感器都是用的Ctrl大法,但是总不能一直复制粘贴,想提升还是需要自己亲自手写,结果磕磕碰碰搞了一天才弄好,各种小问题,以下是我自己写的一篇关于驱动使用单总线协议DHT11的文章,便于记录过程以及遇到的问题。有疑问可以评论,虽然我不咋看0.0
一 开发工具介绍
开发平台: STM32CubeMX(省去写好多初始化代码)
主控模块:STM32F103C8T6(简单说下,其他系列也没问题)
硬件模块:USB转TTL模块(因为要打印调试信息) DHT11传感器模块 STLINK下载器
二 DHT11传感器 简要介绍
1 DHT11接线
对了,DHT11一次工作流程会发过来40位的数据,也就是5个字节。
2 单总线协议 简述
单总线就三根线,电源,接地和数据线,因为线少,所以成本很低,而且节省硬件资源,但所有的数据放到一根线非常容易受到外界和硬件本身各种干扰,所以通信时间慢,传输距离也不远,这就导致某些场景并不适合使用单总线,所以就有了后续的IIC SPI CAN等更牛叉的总线协议。
单总线通过拉高或拉低数据线的电平保持时间来实现开始信号,应答信号和读写信号等。
这张图是DHT11的通讯过程,可以看到STM32的开始信号和DHT11的响应信号,以及STM32的写1和写0都是通过电平持续时间决定的。
比如这张图,DHT11会将数据线拉低50us,然后拉高数据线70us就能表示逻辑‘1’,其它信号也类似。
三 开始编写代码
1 配置STM32CubeMX
enn.我这里找了PB0端口与DHT11通信,关于GPIO口的配置如下图所示
这里配置成:初始状态为高电平-上拉-开漏输出模式
初始状态保持高电平,但因为开漏输出模式下,高电平没办法驱动电流,所以是处于高阻态,但无所谓我们有上拉电阻,当高阻态时,上拉电阻会把数据线拉到真正的高电平状态,其实使用推挽输出更方便,也不需要上拉电阻,但是当总线设备多了之后,万一多台设备有的要拉低数据线有的要拉高数据线,那岂不就产生电平竞态了吗,会短路的,但是使用开漏模式,不管多少设备都只能拉到低电平,这样就可以避免电平冲突了,能够实现和多台设备通信,而且当所有的设备都不拉低数据线时,上拉电阻会把电平拉回到高电平。这里开漏+上拉和推挽效果一样,都能用。我这用的推挽。
我用了串口1来调试用的,使用的printf重定向。要是乱码就看看串口助手和keil的编码格式一样不,我这里注释都是GB2312的
#include "stdio.h"
int fputc(int ch,FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
// while((USART1->SR & 0x40) == 0){}; //等待发送完成
// USART1->DR = ch;
return ch;
}
微妙延时函数使用的定时器1,只需要cube上配置成71MHz就行(C8T6最高主频72),keil中记得引用tim.h文件。用Systick定时器也可以,这个不用配置,直接拿来用。
void delay_us(uint16_t nus)
{
uint16_t differ = 0xffff-nus-5;
__HAL_TIM_SET_COUNTER(&htim1,differ); //设定TIM1计数器起始值
HAL_TIM_Base_Start(&htim1); //启动定时器
while(differ < 0xffff-5){ //判断
differ = __HAL_TIM_GET_COUNTER(&htim1); //查询计数器的计数值
}
HAL_TIM_Base_Stop(&htim1);
}
//void delay_us(uint16_t nus)
//{
// SysTick->LOAD = 72*nus; //设置定时器重装值
// SysTick->VAL = 0x00; //清空当前计数值
// SysTick->CTRL = 0x00000005; //设置时钟源为HCLK,启动定时器
// while(!(SysTick->CTRL & 0x00010000)); //等待计数到0
// SysTick->CTRL = 0x00000004; //关闭定时器
//}
下面是 输入和输出模式。 DHT11是cube上PB0的别名,因为输入输出都是一根信号,所以涉及到模式转换,输出模式下发送完开始信号,就换成输入模式接收数据。
void DHT11_IN(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
GPIO_InitStructure.Pin = DHT11_Pin;
GPIO_InitStructure.Pull = GPIO_PULLUP;//画个重点,拉高电平
HAL_GPIO_Init(DHT11_GPIO_Port,&GPIO_InitStructure);
}
void DHT11_OUT(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStructure.Pin = DHT11_Pin;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(DHT11_GPIO_Port,&GPIO_InitStructure);
}
看下面这个图,数据线开局是高电平,拉低18ms,之后由上拉电阻拉高,保持20-40us,这就是开始信号,发送完换成输入模式开始检测响应信号,DHT11会把数据线拉低80us,在拉高80us这就是DHT11给我们的应答信号。
开始信号和检测应答信号就根据时序图写。
void DHT11_Start(void)
{
DHT11_OUT();
DHT11_IO_LOW
HAL_Delay(18);
DHT11_IO_HIGH
delay_us(30);
}
uint16_t DHT11_Check_Ack(void)
{
DHT11_IN(); //改成输入模式
while(DHT11_Read_IO); 当读取到低电平退出循环
while(!DHT11_Read_IO);//当读取到高电平退出循环
// printf("收到响应信号\r\n");
return 0; //返回0说明DHT11成功应答,不然会死在循环里
}
下面是读取字节函数,循环8次读取8bit数据,DHT11发送给我们响应信号后会陆续发送过来40bit数据。因为上一次发完应答信号是高电平,所以从检测低电平开局。
拉低50us,再拉高26-28us 表示发过来逻辑‘0’;拉低50us,再拉高70us 表示发过来逻辑‘1’,
我这里用了循环判断高低电平。一开始循环检测是不是进入了50us的准备区段,是的话刚好跳出循环,在判断是不是拉高电平了,取反判断,当拉高了电平,取反为0跳出循环,之后开始判断发过来的是‘0’还是‘1’。延时35us,if判断如果还是高电平就是发来了’1‘,要是低电平说明发的’0‘
因为先发过来的是高位,所以左移一位,8此循环左移后就是一个8bit数据。
不明白建议手写8次循环过程,比如0110 0010带入循环。
uint8_t DHT11_ReadByte(void)
{
uint8_t i;
uint8_t dat=0;
for(i=0;i<8;i++)
{
while(HAL_GPIO_ReadPin(DHT11_GPIO_Port,DHT11_Pin));
while(!DHT11_Read_IO);
delay_us(35);
//判断和接收 ’0‘或’1‘
dat = dat << 1;
if(DHT11_Read_IO)
{
dat = dat | 0x01;
}
}
return dat;
}
然后是读取40bit数据了,其实就是整合上边代码,发送开始信号->检测应答信号->循环5次接收40bit的数据,每次接收8bit存入数据->判断校验和(其实我感觉这一步没啥用)->将温度和湿度的整数部分放入变量中,这里是指针参数,可以直接改变变量值.完结撒花~
void DHT11_ReadData(uint8_t *temperature,uint8_t *humidity)
{
uint8_t i;
uint8_t dat_buf[5];
DHT11_Start();
if(!DHT11_Check_Ack())
{
for(i=0;i<5;i++)
{
dat_buf[i] = DHT11_ReadByte();
}
if((dat_buf[0]+dat_buf[1]+dat_buf[2]+dat_buf[3]) == dat_buf[4])
{
*temperature = dat_buf[2];
*humidity = dat_buf[0];
printf("read succeed\r\n");
}
}
else
{
printf("read fail");
}
}
下面是dht11.c的驱动程序
#include "dht11.h"
// 引脚宏定义, DHT11是PB0引脚的别名
#define DHT11_IO_HIGH HAL_GPIO_WritePin(DHT11_GPIO_Port,DHT11_Pin,GPIO_PIN_SET);
#define DHT11_IO_LOW HAL_GPIO_WritePin(DHT11_GPIO_Port,DHT11_Pin,GPIO_PIN_RESET);
#define DHT11_Read_IO HAL_GPIO_ReadPin(DHT11_GPIO_Port,DHT11_Pin)
// DHT11温湿度小数部分采集不到,直接定义两个变量接收整形数据
uint8_t temp = 0;
uint8_t humi = 0;
void DHT11_IN(void) //配置成输入模式,当发送完开始信号,就改为这个输入模式,酷酷接收数据
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
GPIO_InitStructure.Pin = DHT11_Pin;
GPIO_InitStructure.Pull = GPIO_PULLUP; //这个上拉很重要,它能帮DHT11拉高数据线
HAL_GPIO_Init(DHT11_GPIO_Port,&GPIO_InitStructure);
}
void DHT11_OUT(void) //stm32cube上配置的也是这个,没啥用
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStructure.Pin = DHT11_Pin;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(DHT11_GPIO_Port,&GPIO_InitStructure);
}
void DHT11_Start(void)
{
DHT11_IO_LOW
HAL_Delay(20);
DHT11_IO_HIGH
delay_us(30);
}
uint16_t DHT11_Check_Ack(void)
{
DHT11_IN(); //RESET -- 0
while(DHT11_Read_IO);
while(!DHT11_Read_IO);
// printf("收到响应信号\r\n");
return 0;
}
uint8_t DHT11_ReadByte(void)
{
uint8_t i;
uint8_t dat=0;
for(i=0;i<8;i++)
{
// printf("5");
while(HAL_GPIO_ReadPin(DHT11_GPIO_Port,DHT11_Pin));
// printf("7");
while(!DHT11_Read_IO);
// printf("6");
delay_us(35);
dat = dat << 1;
if(DHT11_Read_IO)
{
dat = dat | 0x01;
// printf("2");
}
}
return dat;
}
void DHT11_ReadData(uint8_t *temperature,uint8_t *humidity)
{
uint8_t i;
uint8_t dat_buf[5];
DHT11_Start();
if(!DHT11_Check_Ack())
{
// printf("1");
for(i=0;i<5;i++)
{
dat_buf[i] = DHT11_ReadByte();
}
// printf("4");
if((dat_buf[0]+dat_buf[1]+dat_buf[2]+dat_buf[3]) == dat_buf[4])
{
*temperature = dat_buf[2];
*humidity = dat_buf[0];
printf("read succeed\r\n");
}
}
else
{
printf("read fail");
}
}
下面是dht11.h的驱动程序
#ifndef __DHT11_H__
#define __DHT11_H__
#include "stdio.h"
#include "main.h"
#include "tim.h"
#include "delay.h"
void DHT11_Start(void);
uint16_t DHT11_Check_Ack(void);
uint8_t DHT11_ReadByte(void);
void DHT11_ReadData(uint8_t *temperature,uint8_t *humidity);
#endif
串口打印结果
总结
总结下遇到的问题
1.因为是毫微妙级别时序,所以对延时函数有要求,务必使用定时器实现的延迟函数
2.因为喜欢用printf打印调试信息,所以导致某些时序延时时间过长。
3.使用开漏输出一定要用开启上拉