第022课 传感器

来自百问网嵌入式Linux wiki

第001节_光敏电阻的使用

这节课我们开始讲的传感器,有光敏电阻、DH11温湿度传感器、DS18B20温度传感器、HS0038红外接收器。

首先介绍光敏电阻传感器。

光敏电阻有一个特点,就是它的阻值随光照强度变化而变化,

看一下它的原理图,R5是一个普通电阻,LAS1是光敏电阻,它们串联组成一个分压电路,

LAS1阻值变化,将会导致中间RES_AO测得的电压发生变化。

Chapter22 lesson1 001.png

这个电路图有点绕,画一个示意图如下:

Chapter22 lesson1 002.png

使用ADC测量A点的电压,可以得知LAS1的变化情况,这里的测量是一个模拟信号。


现在假如需要这个光敏系统在光照大于/小于某个值时,发出中断,怎么办呢?

这里就需要再加一个比较的电路,B处的电压是可调电阻得到的电压,可以通过调节电阻进行变化。A、B两个电压最后接在一个比较器的“正负端”,当A>B时,输出1,反之输出0。

通过调节可调电阻,可以实现对比较阈值的控制。

现在这个电路就即可得到模拟信号和数字信号。


现在就可以开始写程序了,复制前面024的代码为025_sensors,这是这一章的第一个项目,再创建个001_photoresistor文件夹,将代码都放进去。

再创建一个sensors文件夹用来放本课的所有传感器代码,然后再创建photoresistor文件夹放本节课代码,再在里面创建photoresistor.c。

在代码里面,我们要做两件事:

1.启动ADC,读出AIN1电压值;
2.注册中断,当光线强度超过或小于某个阈值时,产生中断;

打开工程里的adc代码,原来的adc_init,只初始化了adc0,现在我们要用AIN1,修改下代码,传入个参数就能初始化对应的AIN:

void adc_init(int channel)
{
	/* [15] : ECFLG,  1 = End of A/D conversion
	 * [14] : PRSCEN, 1 = A/D converter prescaler enable
	 * [13:6]: PRSCVL, adc clk = PCLK / (PRSCVL + 1)
	 * [5:3] : SEL_MUX, 000 = AIN 0
	 * [2]   : STDBM
	 * [0]   : 1 = A/D conversion starts and this bit is cleared after the startup.
	 */
	ADCCON = (1<<14) | (49<<6) | (channel<<3);

	ADCDLY = 0xff;	
}

后面的adc_read_ain0只能读取AIN0的数据,现在修改一下,传入个通道参数,就能读取对应通道的ADC值:

int adc_read(int channel)
{
	adc_init(channel);
	
	/* 启动ADC */
	ADCCON |= (1<<0);

	while (!(ADCCON & (1<<15)));  /* 等待ADC结束 */

	return ADCDAT0 & 0x3ff;
}

修改了这两个函数,原来的adc_test函数里调用的adc读取函数也要对应进行修改。


参考这个adc_test,编写photoresistor_test函数。

需要修改的内容并不多,首先是修改adc_read参数,将通道0改为通道1。

然后我们想同时再把AIN0上的滑动电阻对应的电压也读出来,因此再做一次ADC0读取的操作。

void photoresistor_test(void)
{
	int val, val0;
	double vol, vol0;
	int m, m0; /* 整数部分 */
	int n, n0; /* 小数部分 */
	
	//adc_init();

	while (1)
	{
		val = adc_read(1);
		vol = (double)val/1023*3.3;   /* 1023----3.3v */
		m = (int)vol;	/* 3.01, m = 3 */
		vol = vol - m;	/* 小数部分: 0.01 */
		n = vol * 1000;  /* 10 */

		val0 = adc_read(0);
		vol0 = (double)val0/1023*3.3;   /* 1023----3.3v */
		m0 = (int)vol0;	/* 3.01, m = 3 */
		vol0 = vol0 - m0;	/* 小数部分: 0.01 */
		n0 = vol0 * 1000;  /* 10 */

		/* 在串口上打印 */
		printf("photoresistor vol: %d.%03dv, compare to threshold %d.%03dv\r", m, n, m0, n0);  /* 3.010v */

		/* 在LCD上打印 */
		//fb_print_string();
	}

以上就完成了我们的第一个目标。


现在开始做第二个目标,注册中断放在interrupt.c里实现。

硬件上RES_DO是EINT15。我们可以仿照之前的按键中断来编写本次的中断。

先分析一下中断,如图:

Chapter22 lesson1 003.png


GPG7作为中断引脚,它会先经过外部中断EINTMASK寄存器,才能进入到中断控制器。

所以,需要做以下操作:

  • ①首先初始化:
a.GPG7配置为中断引脚;
b.设置中端触发方式:双边沿触发;
c.设置EINTMASK使能中断;
  • ②中断处理:
a.分辨:读EINTPEND;
b.读GPG7;

在原来的key_eint_init函数里配置GPG7为中断引脚:

	/* 配置GPG7为中断引脚, 用于光敏电阻 */
	GPGCON &= ~((3<<14));
	GPGCON |= ((2<<14));

然后设置中端触发方式:双边沿触发

	/* 设置中断触发方式: 双边沿触发 */
	EXTINT0 |= (7<<0) | (7<<8);     /* S2,S3 */
	EXTINT1 |= (7<<12);             /* S4 */
	EXTINT2 |= (7<<12);             /* S5 */

最后再使能中断:

	/* 使能中断GPG7/EINT15, 用于光敏电阻 */
	EINTMASK &= ~((1<<15));

修改key_eint_irq中断处理函数,先判断是哪个中断产生,再读取电平,打印。

else if (val & (1<<15)) /* eint15, 用于光敏电阻 */
		{
			if (val2 & (1<<7))
			{
				printf("\n\rphotoresistor dark!\n\r");
			}
			else
			{
				printf("\n\rphotoresistor light!\n\r");
			}
		}

至此,代码就写完了,最后还要修改对应的Makefile和主函数。

第002节_高精度延时函数

在后续我们对讲解多个传感器,这几个传感器对时序的要求都比较高,比如温湿度传感器DH11,查看芯片手册时序,至少就需要微秒级的延时函数。

延时函数的方式一般有两种:

  • ①:使用for循环,利用示波器等工具测得精确值;
  • ②:使用定时器,通过不断检测定时器的计数值获得精确时间;


使用for循环的方式,可能会因为硬件的差异,导致延时函数不准,因此这里我们使用定时器的方式。

打开之前的timers.c文件,修改timer_init函数的配置。

PCLK仍然等于50000000,将prescaler value改为4,divider value设置为2,

这样,每减1, 对应0.2us;每减5, 对应1us;从50000减到0,对应10ms。

修改对应的寄存器:

	TCFG0 = 4;  /* Prescaler 0 = 4, 用于timer0,1 */
	TCFG1 &= ~0xf; /* MUX0 : 1/2 */

	/* 设置TIMER0的初值 */
	TCNTB0 = 50000;  /* 10Ms中断一次 */

我们先写一个us延时的函数,然后ms延时就调用us即可。

因此,us延时函数里,尽量少调用函数。

假如现在要延时nus,我们先将n*5,得到nus对应的“计数时钟数”。

然后如果传入“计数时钟周期”如果大于0,则一直计算过去了多少个“计数时钟数”,与传入的“计数时钟数”相减,直到为零,退出循环,也就实现了延时nus。


怎样计算过去了多少个“计算周期”呢?

自然是当前的值,减去一开始进入函数的值。

但还有一种情况是定时器里的计数记到0时,会自动变成5000,计数计数,这时候,计算方式就变成了pre+(5000-cur):

Chapter22 lesson2 001.png

/* 尽量少调用函数 */
void udelay(int n)
{
	int cnt = n * 5;  /* u us 对应n*5个计数值 */
	int pre = TCNTO0;
	int cur;
	int delta;

	while (cnt > 0)
	{
		cur = TCNTO0;
		if (cur <= pre)
			delta = pre - cur;
		else
			delta = pre + (50000 - cur);

		cnt = cnt - delta;
		pre = cur;
	}
}

然后时ms延时函数:

void mdelay(int m)
{
	udelay(m*1000);
}

我们可以写一个测试函数,简单的测试下是否可用,测试函数隔1分钟进行打印一下。

如果us不准的话,放大至s,会有比较大的偏差,这样可以进行粗略的检测,精确检测可以使用示波器等工具。

void hrtimer_test(void)
{
	int cnt = 0;
	while (1)
	{
		printf("delay one min: ");
		mdelay(60000); /* 延时1分钟 */
		printf("%d\n\r", ++cnt);
	}
}


前面延时里的计算还是比较耗费时间的,因此,我们尽量提高CPU的运行时钟,并且 将尽可能的启动icache、dcache和mmu。

此外,如果延时过程中,发生了中断,如果中断比较耗时的话,就会导致延时可能出现不准确,所以,我们可以延时之前关中断, 延时之后开中断;

课后作业:

  • a. 禁止icache, 禁止mmu, 修改lds, 测试延时函数是否还准确;
  • b. 测试延时之前关中断, 延时之后开中断;

第003节_DHT11温湿度传感器的使用

这节课开始讲解DH11温湿度传感器的使用,首先查看芯片手册,里面的典型应用电路如下:

Chapter22 lesson3 001.png

MCU通过一条数据线与DH11连接,MCU通过这条线发命令给DH11,DH11再通过这条线把数据发送给MCU。

因此,温湿度模块的核心就是 MCU发给DH11的命令格式和DH11返回的数据格式。

再来先简单看一下通讯的时序:

Chapter22 lesson3 002.png

灰色这条线是由MCU驱动控制的,浅色的部分是由DH11驱动控制的。

首先MCU发送一个开始信号,这个开始信号是一个低脉冲,然后再拉高。

然后,DH11拉低,做出一个响应信号,再拉高。

接着就是DH11返回的数据。


这些数据一共有40bit,高位先出。

数据格式:8bit湿度整数数据+8bit湿度小数数据+8bi温度整数数据+8bit温度小数数据+8bit校验和

且当前小数部分用于以后扩展,现读出为零.

数据传送正确时校验和数据等于“8bit湿度整数数据+8bit湿度小数数据+8bi温度整数数据+8bit温度小数数据”所得结果的末8位。


DH11的难点是前面所说的时序脉冲,需要满足一定的时长.比如开始信号:

Chapter22 lesson3 003.png

MCU必须先拉低至少18ms,然后再拉高20-40us,DH11再拉低80us以响应,最后再拉高80us.


接下来就是传输数据,我们的目的就是读到温湿度的数据,这些数据由DH11提供,那它怎么传回这些数据,怎么表示0和1呢?

Chapter22 lesson3 004.png

Chapter22 lesson3 005.png


可以看到,不管是0还是1,都开始是50us的低电平,

对于0数据,之后是26~28us的高电平;

对于1数据,之后是70us的高电平;


有了上面的知识,加上之前的高精度延时,现在就可以开始写程序了。

复制前面的第二个程序,文件名改为003_dht11_022_003,然后在sensors目录里新建dht11目录,再创建一个dht11.c文件。

我们的目的是,控制GPIO读取DHT11的数据,流程如下:

  • 1. 主机发出至少18MS的低脉冲: start信号
  • 2. start信号变为高, 20-40us之后, dht11会拉低总线维持80us,然后拉高80us: 回应信号
  • 3. 之后就是数据, 逐位发送
bit0 : 50us低脉冲, 26-28us高脉冲
bit1 : 50us低脉冲, 70us高脉冲
  • 4. 数据有40bit: 8bit湿度整数数据+8bit湿度小数数据+8bit温度整数数据+8bit温度小数数据+8bit校验和

DH11的DATA引脚连接到了GPG5。

先实现GPIO的基本操作,配置GPIO模式,实现输出、输入引脚的功能:

static void dht11_data_cfg_as_output(void)
{
	GPGCON &= ~(3<<10);
	GPGCON |= (1<<10);
}

static void dht11_data_cfg_as_input(void)
{
	GPGCON &= ~(3<<10);
}


再设置输出电平或读取引脚数据:

static void dht11_data_set(int val)
{
	if (val)
		GPGDAT |= (1<<5);
	else
		GPGDAT &= ~(1<<5);
}

static int dht11_data_get(void)
{
	if (GPGDAT & (1<<5))
		return 1;
	else
		return 0;
}


再来实现DHT11的读操作。

在芯片手册里介绍说,DH11传感器上电后,要等待1s,以越过不稳定状态,在此期间无需发送任何指令。

因此首先写一个初始化函数,跳过这个不稳定状态:

void dht11_init(void)
{
	dht11_data_cfg_as_output();
	dht11_data_set(1);
	mdelay(2000);
}


根据start时序要求,编写程序,维持一个大于18ms的低电平,然后释放引脚,及设置为输入引脚即可。

因为该引脚接有上拉电阻,一旦MCU设置为输入,引脚电平将有上拉电阻决定。

static void dht11_start(void)
{
	dht11_data_set(0);
	mdelay(20);
	dht11_data_cfg_as_input();
}

然后等待40us以上,再去读取引脚电平,判断是否被拉低,以确定DH11给了响应。

static int dht11_wait_ack(void)
{
	udelay(60);
	return dht11_data_get();
}

再写个延时函数,用于时序中的,等待响应信号结束:

static int dht11_wait_for_val(int val, int timeout_us)
{
	while (timeout_us--)
	{
		if (dht11_data_get() == val)
			return 0; /* ok */
		udelay(1);
	}
	return -1; /* err */
}

后面的数据会有五个字节组成,这里先写出读取一个字节,每个字节要读取8位。

先等待直到高电平,过滤到共同的50us延时,然后延时28us以上,再读取引脚电平,

如果引脚电平是1,则数据是1,反之是0。

然后再直到低电平的到来,循环8次,完成一个字节数据的读取。

static int dht11_recv_byte(void)
{
	int i;
	int data = 0;
	
	for (i = 0; i < 8; i++)
	{
		if (dht11_wait_for_val(1, 1000))
		{
			printf("dht11 wait for high data err!\n\r");
			return -1;
		}
		udelay(40);
		data <<= 1;
		if (dht11_data_get() == 1)
			data |= 1;
		
		if (dht11_wait_for_val(0, 1000))
		{
			printf("dht11 wait for low data err!\n\r");
			return -1;
		}
	}
	
	return data;
}

第004节_DS18B20温度传感器介绍

第005节_DS18B20温度传感器编程

第006节_红外线遥控协议简介及编程思路

本节开始讲解红外遥控器信号的接收和解码,视频分为三部分,每一部分都专注做一件事情,让每节视频更短一点。

红外遥控器的操作比前面的温度、温湿度传感器都要简单。

首先看一下原理图上的红外遥控接收器:

Chapter22 lesson6 001.png

我们用遥控器对它按动的时候,它就可以接收到红外信号,然后把红外信号转换成电平信号,通过IRD这根线,传给SOC。

整个传输,只涉及单向传输,由HS0038向主芯片传送。

因此,我们只需要编写程序,从IRD上获取数据即可,在这之前,我们需要先了解下数据是怎么表示的,也就是传输的红外数据的格式。

红外协议有:NEC、SONY、RC5、RC6等,常用的就是NEC格式,因此我们主要对NEC进行讲解。


可以参考这个文章,直观的了解下NEC格式波形的样子: https://www.cnblogs.com/openusb/archive/2010/01/07/1641357.html

在分析文章中的波形之前,我们先想象一下怎么在一条数据线上传输信号。

开始传输数据之前,一般都会发出一个start起始信号,通知对方我开始传输数据了,后面就是每一位每一位的数据。

NEC协议的开始是一段引导码:

Chapter22 lesson6 002.png

这个引导码由一个9ms的低脉冲加上一个4.5ms的高脉冲组成,它用来通知接收方我要开始传输数据了。

Chapter22 lesson6 003.png

然后接着的是数据,数据由4字节组成:地址、地址(取反)、数据、数据(取反),这些取反是用来校验用的。

地址是指遥控器的ID,每一类遥控器的ID都不一样,数据就是遥控器上的不同按键。

从前面的图可以知道,NEC每次要发32位的数据,每一位用什么来表示0和1呢?

Chapter22 lesson6 004.png

数据1和01,开始都是0.56ms的低脉冲,对于数据1,后面的高脉冲比较长,对于数据0,后面的高脉冲比较短。 可以看出,红外遥控器的数据表示方法是比较简单的。


我们长按一个按键,第一次按的时候,他会发出引导码,地址,地址取反,数据,数据取反。

接着由于长按,遥控器会发送一个不一样的引导码,这个引导码由9ms的低脉冲,2.25ms的高脉冲组成,表示现在按的还是上次一样的按键,然后再一直是引导码(重复),直到松开。

Chapter22 lesson6 005.png


在后面的调试中,发现以上并不是NEC协议的全部,打开bing国际版搜索“ir nec protocal”,得到一篇官方文章:http://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol

里面的内容和前面文章基本一致,但这个更详细,发现每次数据传输完还有一个0.5625ms的低脉冲表示数据传输结束。

对于引导码(重复)也一样,也有一个0.5625ms的低脉冲表示传输结束。

大部分文章都漏掉了结束的低脉冲。


NEC协议里有很多时间,这些时间有一个有趣的现象,把所有时间里面最小的0.53ms看作基本脉冲宽度,假设用t表示,那么其它所有时间都是t的倍数:

Chapter22 lesson6 006.png

我们可以看到对于所有的时间,最小的单位都是0.56ms,这个时间对人来说是非常短的,但对嵌入式系统它是非常非常长的了,足够我们做很多事情了,那么我们可以使用中断来处理这些数据。

并且对于红外遥控器来说,我们根本不知道用户什么时候按下遥控器,使用轮询的方式特别耗资源,因此直接使用中断来处理。


使用官方文档的时序图: Chapter22 lesson6 007.png

图中的脉冲方向正好相反,绿色表示低脉冲,白色表示高脉冲。

涉及内容:

  • ①中断引脚设置为双边缘触发,在每一个脉冲变化的地方都会产生中断;
  • ②发生中断时,计算当前中断与上次中断之间的时间差,就得到脉冲宽度,放入buffer,同时还要记录引脚极性;
  • ③主循环从buffer取出数据,并解析时序;

我们可以估算下,每按下一次遥控器,会产生多少中断,2+32*2+1=67次。

中断发生时,将数据放入buffer,主函数从buffer取出数据,用什么数据结构来实现数据的存取?

最好的方式就是环形缓冲区,所谓环形缓冲区就是一边存储数据一边读取数据,下节课再详细讲解。


编程要点:

  • ①中断
  • ②系统时间
  • ③环形缓冲区
  • ④NEC解析

第007节_前期编程_系统时间与环型缓冲区

这节课实现两个小功能:系统时间环形缓冲区

在上一课的基础上添加代码,打开timer.c,前面的设置的定时器,每10ms产生一次中断,这里定义一个全局变量,来记录产生次数,

static unsigned int g_system_time_10ms_cnt = 0;

这里的类型选择使用unsigned int类型,2^32*10/1000/3600/24=497天,也就是说如果运行497天后,计数溢出,将会导致一些问题。

改为unsigned long long类型的话,2^64*10/1000/3600/24/365=5849424173年,这个时间就不怕溢出了,因此这里计数变量的类型为unsigned long long

static unsigned long long g_system_time_10ms_cnt = 0;

在定时器中断timer_irq()函数里面让这个计数值每次加1:

g_system_time_10ms_cnt++;

以后我们就可以读取这个计数值,知道系统时间。

现在开始编写获取系统时间的函数,精度要求是us级别的,读取TCNTO0的值,再加上g_system_time_10ms_cnt的计数:

unsigned long long get_system_time_us(void)
{
	unsigned long long us = (50000 - TCNTO0)/5;
	return g_system_time_10ms_cnt * 10 * 1000 + us;
}

通过这个函数就知道上电到之后任何一个时刻,过去了多久。

再写一个函数计算两段时间之间的差值:

unsigned int delta_time_us(unsigned long long pre, unsigned long long now)
{
	return (now - pre);
}

到此,系统时间相关函数就完成了。

首先介绍一下环形缓冲区,假设有一个数组char Buf[6],它的结构如下: Chapter22 lesson7 001.jpg

定义一个读指针r=0,一个写指针w=0

  • 写数据:

buf[w] = val;

w = (w+1)%LEN = (w+1)%6;

  • 读数据:

val = buf(r);

r = (r+1)%LEN;


如何判断buf是空: r == w; //为空

如何判断buf是满: (w+1)%LEN == r; //为满

读写指针,每到达最后面,就从0开始,就像一个圆环一样,因此得名环形缓冲区。

对于我们红外数据,保存的数据并不是char,而是一个结构体,里面含有脉冲宽度,引脚极性等。

在sensors文件下创建一个irda文件夹,里面创建irda_raw.h和circle_buffer.c,在irda_raw.h里定义一个数据结构体,包含极性和脉冲宽度:

#ifndef _IRDA_RAW_H
#define _IRDA_RAW_H

typedef struct irda_raw_event {
	int pol; /* 极性 */
	int duration;  /* 脉冲宽度, us */
}irda_raw_event, *p_irda_raw_event;

#endif /* _IRDA_RAW_H */

然后在circle_buffer.c实现环形缓冲区。

先定义个irda_raw_event类型的g_events[]数组,这里大小设置为1024,

之前介绍过,每传一次irda,至少会传67次数据,因此这个buf要至少大于67行,再定义两个读写指针位置。

static irda_raw_event g_events[1024];
static int g_r = 0;
static int g_w = 0;

判断buf是否是空的函数:

static int is_ir_event_buf_empty(void)
{
	return g_r = g_w;
}

判断buf是否是满的函数:

static int is_ir_event_buf_full(void)
{
	return NEXT_PLACE(g_w) == g_r;
}

其中,(w+1)%LEN使用宏NEXT_PLACE(i)代替,宏的定义如下:

#define NEXT_PLACE(i) ((i+1)&0x3FF)

%的操作使用位&操作实现一样的效果。

然后是把数据放入缓冲区:

int ir_event_put(p_irda_raw_event pd)
{
	if (is_ir_event_buf_full())
		return -1;
	g_events[g_w] = *pd;
	g_w = NEXT_PLACE(g_w);
	return 0;
}

先判断的缓冲区是否已满,没满的话就在写的位置放入数据,然后写位置再移动到下一个。

最后是读数据:

int ir_event_get(p_irda_raw_event pd)
{
	if (is_ir_event_buf_empty())
		return -1;
	*pd = g_events[g_r];
	g_r = NEXT_PLACE(g_r);
	return 0;
}

先判断的缓冲区是否是空,没空的话就在读的位置读出数据,然后读位置移到到下一个。


修改Makefile,添加本次写的新文件。

第008节_HS0038红外线接收器的编程_打印原始脉冲

第009节_HS0038红外线接收器的编程_解析数据