第022课 传感器

来自百问网嵌入式Linux wiki
Wiki讨论 | 贡献2018年3月27日 (二) 20:41的版本

第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温度传感器介绍

比DHT11温湿度传感器精度高很多

Chapter22 lesson4 001.png

DS18B20只通过一条数据线传输数据,既要控制器发送数据给芯片,又要通过芯片发送数据给控制器,所以这个是双向传输数据的

怎么在一个引脚上实现数据的双向传输 : 参考这视频的第19分钟之后的内容: 第19课_第001节_I2C协议与EEPROM

检测温度,我们需要一个主控芯片

如果有多个温度传感器,则需要一个主控制器去管理它们,通过发送命令传输数据,每个设备都会有固化在芯片内部的64bit ID的ROM来用于区分不同的设备

如果主控制器想访问设备,必须发送命令,这个命令中带有ID返回值

怎么访问指定的DS18B20 0 发出低脉冲,提醒准备工作: initialization 1 发出ID命令:ROM Command 2 发出功能命令: Function Command a转换温度 b读温度,读数据

每次操作,都要重重上述过程

内部框图 Chapter22 lesson4 002.png

TEMPERATURE SENSOR温度ADC

SCRATCHPAD实际上是一个9 byte的内存 9byte的说明如下图所示

Chapter22 lesson4 003.png

温度值会保存在9byte内存中的 BYTE0 和 BYTE1 也就是当我们发出一个温度值的命令之后,还需要发送一个读内存的命令才能把温度值读取出来

最后一位是CRC校验码,通过前8位的数据和最后一位的校验码比较 64位数据中有8位是校验码,

怎么采样温度?

  • 1 初始化
  • 2 ROM命令
  • 3 FunctionCommand 设置某些值,比如转换温度
  • 4等待完成
  • 5 init
  • 6 R om cmd
  • 7 Function Command 读RAM中的值

Chapter22 lesson4 004.png

关于EEPROM寄存器 前面两个字节可以用来设置用户自己的目的,也可以用来设置Th Tl 寄存器 Th Tl 寄存器就是用来设置警报,温度的上限或者下限,当温度超过某个值时它会发出警报 所谓警报只不过是在DS18B20中设置状态而已,并不能主动通知主芯片 主芯片可以发出某些命令来确定那些芯片发出了警报 配置寄存器,用于设置精度,精度越高转换时间越长

EEPROM使用 1 上电 EEPROM自动放入RAM用于控制精度这些

2 写EEPROM a 发出命令先写RAM b从RAM传到EEPROM 3 读出EEPROM的值 a EEPROM值保存到RAM b 发出命令读RAM

关于ROM命令和功能命令整理成一个表格

ROM Commands 命令名称 描述
F0H Search ROM 搜索ROM 用于确定挂接在同一总线上DS18B20的个数,识别64位ROM地址
33H Read ROM 读ROM 读DS18B20芯片中的编码值,即64位ROM值
55H Match ROM 匹配ROM 发出此命令后,接着发出64位ROM编码,用于选中某个设备
CCH Skip ROM 忽略ROM 表示后续发出的命令将会发给所有设备 如果总线上只有一个DS18B20,则特别适用此命令
ECH Alarm ROM 警报搜索 执行此命令后,只有温度超过设定值上限或下限的芯片才会做出响应
Function Commands 命令名称 描述
44H Convert Teamperature 启动温度转换,注意不同精度需要不同转换时间,

结果存入内部RAM

4EH Write Scratchpad 写内部RAM,可以写入3字节:TH,TL,配置值(用于选择精度)TH,TL可用于设置报警上下限,或给用户自己使用
BEH Read Scratchpad 读整个内部RAM,9字节
48H Copy Scratchpad 把内部RAM中的TH、TL、配置值,复制给EEPROM
B8H Recall EEPROM 从EEPROM中把TH、TL、配置值,读到内部RAM
B4H Read Power Supply 分辨DS18B20的供电方式:用电源引脚供电,或从数据线偷电

信号传输 1怎么initialization(初始化) 2 怎么发数据,怎么发出1bit 怎么发出bit0 怎么发出bit1 3怎么读数据==>怎么读1bit 怎么判读读到0 怎么判断读到1

初始化时序 Chapter22 lesson4 005.png

一开始是高电平,想要开始传输信号,必须要拉低至少480us释放总线 经过15~60us之后 DS18B20会把这条线拉低60~240us

2 怎么发数据,怎么发出1bit 怎么发出bit0 怎么发出bit1

写数据时序 Chapter22 lesson4 006.png

不论是写0还是写1时序都是大于60us 写0拉低总线维持60us以上 写1时,信号线拉低1us时间,提醒要写数据了,让后回高,写1位之间的时间间隔1us

Chapter22 lesson4 007.png 读数据时序 也是由主机发起 提醒脉冲大于1us,主机马上释放总线 在15us之内读信号,一个读周期至少是60us,每位的间隔也是1us

DS18B20提供了编程图 Chapter22 lesson4 008.png

供电方式 Chapter22 lesson4 009.png

参考资料: DS18B20 驱动编写 http://blog.csdn.net/zqixiao_09/article/details/50973969

CRC8校验分析和DS18B20的CRC8编程实现方法 http://www.360doc.com/content/15/1230/17/27708084_524223594.shtml

第005节_DS18B20温度传感器编程

设置精度相关的位

Chapter22 lesson4 010.png

创建ds18b20目录 查看使用那个引脚

Chapter22 lesson4 011.png

使用GPG6引脚

#include "../../s3c2440_soc.h"

/* 使用GPG6作用ds18b20的DATA引脚 */

/*定义命令和函数的宏*/
/* rom commands */
#define SEARCH_ROM    0xF0
#define READ_ROM      0x33
#define MATCH_ROM     0x55
#define SKIP_ROM      0xCC
#define ALARM_ROM     0xEC

/* functions commands */
#define CONVERT_TEAMPERATURE 0x44
#define WRITE_SCRATCHPAD     0x4E
#define READ_SCRATCHPAD      0xBE
#define COPY_SCRATCHPAD      0x48
#define RECALL_EEPROM        0xB8
#define READ_POWER_SUPPLY    0xB4


/* 先实现GPIO的基本操作 */
static void ds18b20_data_cfg_as_output(void)
{
	GPGCON &= ~(3<<12);
	GPGCON |= (1<<12);
}

static void ds18b20_data_cfg_as_input(void)
{
	GPGCON &= ~(3<<12);
}

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

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

/*通过函数设置时间
*/
static void ds18b20_data_set_val_for_time(int val, int us)
{
	ds18b20_data_cfg_as_output();
	ds18b20_data_set(val);
	udelay(us);
}

/*
ds18b20释放总线
*/
static void ds18b20_data_release(void)
{
	ds18b20_data_cfg_as_input();
}

/* ds18b20的代码
*先实现ds18b20初始化操作
 */
static int ds18b20_initialization(void)
{
	int val;
	
	ds18b20_data_set_val_for_time(0, 500);
	ds18b20_data_release();
	udelay(80);

	val = ds18b20_data_get();
	udelay(250);
	return val;
}

/*
实现写入一位数据
*/
static void ds18b20_write_bit(int val)
{
	if (0 == val)
	{
		ds18b20_data_set_val_for_time(0, 60);		
		ds18b20_data_release();
		udelay(2);
	}
	else
	{
		ds18b20_data_set_val_for_time(0, 2);		
		ds18b20_data_release();
		udelay(60);
	}
}

/*
实现一位数据的读操作
*/
static int ds18b20_read_bit(void)
{
	int val;
	
	ds18b20_data_set_val_for_time(0, 2);		
	ds18b20_data_release();
	udelay(10);
	val = ds18b20_data_get();
	udelay(50);
	return val;
}

/*
实现写一字节数据函数
*/
static void ds18b20_write_byte(unsigned char data)
{
	/*
	优先传输最低位
	*/
	int i;
	for (i = 0; i < 8; i++)
	{ 
		ds18b20_write_bit(data & (1<<i));
	}
}

/*
实现读一字节数据函数
*/
static unsigned char ds18b20_read_byte(void)
{
	int i;
	unsigned char data = 0;

	for (i = 0; i < 8; i++)
	{
		if (ds18b20_read_bit() == 1)
			data |= (1<<i);
	}

	return data;
}

/*
写入一字节的命令数据
*/
static void ds18b20_write_rom_cmd(unsigned char cmd)
{
	ds18b20_write_byte(cmd);
}

/*
写入一字节的功能命令数据
*/
static void ds18b20_write_function_cmd(unsigned char cmd)
{
	ds18b20_write_byte(cmd);
}

/*
实际操作函数 
读rom
*/

int ds18b20_read_rom(unsigned char rom[])
{
	int i;
	
	if (ds18b20_initialization() != 0)
	{
		printf("ds18b20_initialization err!\n\r");
		return -1;
	}

	ds18b20_write_rom_cmd(READ_ROM);
	
	for (i = 0; i < 8; i++)
	{
		rom[i] = ds18b20_read_byte();
	}

	return 0;
}

int ds18b20_wait_when_processing(int timeout_us)
{
	while (timeout_us--)
	{
		if (ds18b20_read_bit() == 1)
			return 0;  /* ok */
		udelay(1);
	}
	return -1;
}

/*
启动温度传输
*/
int ds18b20_start_convert(void)
{	/*
		所有操作到要先发出初始化操作,再发出rom命令
	*/
	if (ds18b20_initialization() != 0)
	{
		printf("ds18b20_initialization err!\n\r");
		return -1;
	}

	ds18b20_write_rom_cmd(SKIP_ROM);
	ds18b20_write_function_cmd(CONVERT_TEAMPERATURE);

	/* 等待/判断转换成功 */
	if (0 != ds18b20_wait_when_processing(1000000))
	{
		printf("ds18b20_wait_when_processing err!\n\r");
		return -1;
	}

	return 0;	
}

/*
读ram中的数据
*/
int ds18b20_read_ram(unsigned char ram[])
{
	int i;
	
	if (ds18b20_initialization() != 0)
	{
		printf("ds18b20_initialization err!\n\r");
		return -1;
	}

	ds18b20_write_rom_cmd(SKIP_ROM);
	ds18b20_write_function_cmd(READ_SCRATCHPAD);

	for (i = 0; i < 9; i++)
	{
		ram[i] = ds18b20_read_byte();
	}

	return 0;
}

/*
读温度
*/
int ds18b20_read_temperature(double *temp)
{
	int err;
	unsigned char ram[9];
	/*每一位都是前面一位的两倍*/
	double val[] = {0.0625, 0.125, 0.25, 0.5, 1, 2, 4, 8, 16, 32, 64};
	double sum = 0;
	int i;
	
	err = ds18b20_start_convert();
	if (err)
		return err;

	err = ds18b20_read_ram(ram);
	if (err)
		return err;

	/* 计算温度 */

	/* 先判断精度  byte4 的4位和6位*/
	if (ram[4] & (3<<5) == 0) /* 精度: 9bit */
		i = 3;
	else if (ram[4] & (3<<5) == (1<<5)) /* 精度: 10bit */
		i = 2;
	else if (ram[4] & (3<<5) == (2<<5)) /* 精度: 11bit */
		i = 1;
	else
		/* 精度是 12 bit */
		i = 0;
	
	for (; i < 8; i++)
	{
		if (ram[0] & (1<<i))
			sum += val[i];
	}

	for (i = 0; i < 3; i++)
	{
		if (ram[1] & (1<<i))
			sum += val[8+i];
	}

	if (ram[1] & (1<<3))
		sum = 0 - sum;

	*temp = sum;
	return 0;
}

void ds18b20_init_state(void)
{
	ds18b20_data_release();
}

void ds18b20_test(void)
{
	unsigned char rom[8];
	int i;
	double temp;
	int m,n;
	/*一开始我们应该保证为高电平*/
	ds18b20_init_state();
	
	//while (1)
	{
		if (ds18b20_read_rom(rom) == 0)
		{
			printf("ds18b20 rom: ");
			for (i = 0; i < 8; i++)
			{
				printf("%02x ", rom[i]);
			}
			printf("\n\r");
		}
	}

	while (1)
	{
	/*循环打印温度*/
		if (0 == ds18b20_read_temperature(&temp))
		{	/*实现浮点数的打印*/
			m = (int)temp;	/* 3.01, m = 3 */
			temp = temp - m;	/* 小数部分: 0.01 */
			n = temp * 10000;  /* 10 */
			
			/* 在串口上打印 */
			printf("ds18b20 temperature: %d.%04d\n\r", m, n);  /* 3.010v */
		}
	}
}

修改main.c 添加

ds18b20_test();

修改Makefile 添加

objs += sensors/ds18b20/ds18b20.o

由于使用数组涉及到了memcpy 所以编译出错,现在需要实现在string_utils.c实现memcpy函数

void *memcpy(void *dest, const void *src, int count)
{
	char *tmp = dest;
	const char *s = src;

	while (count--)
		*tmp++ = *s++;
	return dest;
}

烧写

课后作业:

  • a. 使用CRC较验 64bit rom数据
  • b. 使用CRC较验 9byte的 ram数据
  • c. 增加写ram, 写eeprom的功能, 设置转换精度

第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红外线接收器的编程_打印原始脉冲

先打印出原始数据

irda_raw.c
irda_raw.h

获取电平极性方式,当前引脚极性电平取反

#include "../../s3c2440_soc.h"
#include "irda_raw.h"

/* IRDA引脚 : EINT1/GPF1 */

static unsigned long long g_last_time = 0;

/* 
 * 配置GPIO, 注册中断
 * 在中断处理函数里:
      记录中断发生的时间,
      跟上次中断的时间比较, 计算出脉冲宽度
      读取引脚极性
      把数据放入环型缓冲区
 */

 /* 先实现GPIO的基本操作 */
static void irda_data_cfg_as_eint(void)
{
	/* 配置为中断引脚 */
	GPFCON &= ~(3<<2);
	GPFCON |= (2<<2);

	/* 设置中断触发方式: 双边沿触发 */
	EXTINT0 |= (7<<4);  /* eint1 */

}

static int irda_data_get(void)
{	/*如果bit1 等于1就表明高电平,返回1 */
	if (GPFDAT & (1<<1))
		return 1;
	else
		return 0;
}

/*irda中断处理函数*/
void irda_irq(int irq)
{
	/* 在中断处理函数里:
		 记录中断发生的时间,
		 跟上次中断的时间比较, 计算出脉冲宽度
		 读取引脚极性
		 把数据放入环型缓冲区
	*/
	irda_raw_event event;
	/*获得当前时间并赋值给cur*/
	unsigned long long cur = get_system_time_us();
	/*上次时间和这次时间的差值,也就是周期*/
	event.duration = delta_time_us(g_last_time, cur);
	/*获取引脚极性*/
	event.pol      = !irda_data_get();
	/*我们需要环形缓冲区的函数放入环形缓冲区 */
	ir_event_put(&event);
	/*更新时间*/
	g_last_time = cur;
}


/* 注册中断 仿照之前的按键中断程序 */
void irda_init(void)
{	/*1. 配置为中断引脚
	* 2. 配置为双边沿触发 
	*/
	irda_data_cfg_as_eint();
	/*注册中断*/
	register_irq(1, irda_irq);
}
/*测试原始数据*/
void irda_raw_test(void)
{
	irda_raw_event event;
	unsigned long long pre = 0, cur;
	
	irda_init();

	while (1)
	{
		/*如果从唤醒缓冲区读到数据,就把它打印出来*/
		if (0 == ir_event_get(&event))
		{
			cur = get_system_time_us();
			/*如果这次时间和上次时间相差远的话,就打印回车换行*/
			if (delta_time_us(pre, cur) > 1000000)
				printf("\n\r");
			pre = cur;
			/*使用三目运算符来判断pol是高电平还是低电平*/
			printf("%s %d us | ", event.pol? "hight" : "low", event.duration);
		}
	}
}

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 */

测试代码 修改main.c 在main主函数中增加

irda_rae_test();

修改Makefile 增加

objs += sensors/irda/irda_raw.o

编译执行

Chapter22 lesson8 001.png

一开始时间很长是因为一上电的时候高电平,值打印的特别大,说明有问题

irda中断函数中没有更新上一次的时间

/*更新时间*/
g_last_time = cur;

再次更新

Chapter22 lesson8 002.png

时间和数据格式符合时序要求

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

解析NEC格式的数据

创建irda_nec.c

NEC解析数据 Chapter22 lesson8 003.png

Chapter22 lesson8 004.png

对于NEC格式的脉冲,基本脉冲宽度是0.56ms = 562.5us = t

其它的脉冲都是这个的整数倍,需要控制脉冲在9ms范围,那么我们可以把这个作为其他脉冲的最大偏差值

9ms – 2/t < DURATION < 9ms + 2/t 得到判断最大偏差值公式

#include "irda_raw.h"

/*
 * 从环型缓冲区中获得脉冲数据,
 * 解析得出address, data
 */


/*持续时间*/
#define DURATION_BASE  563
#define DURATION_DELTA (DURATION_BASE/2)

/*低脉冲头信息*/
#define DURATION_HEAD_LOW    (16*DURATION_BASE)
/*高脉冲头信息*/
#define DURATION_HEAD_HIGH   (8*DURATION_BASE)
/*高脉冲重复码*/
#define DURATION_REPEAT_HIGH (4*DURATION_BASE)
/*低脉冲传输数据*/
#define DURATION_DATA_LOW    (1*DURATION_BASE)
/*数据1高脉冲*/
#define DURATION_DATA1_HIGH  (3*DURATION_BASE)
/*数据0高脉冲*/
#define DURATION_DATA0_HIGH  (1*DURATION_BASE)
/*低脉冲结束码*/
#define DURATION_END_LOW     (1*DURATION_BASE)

static int duration_in_margin(int duration, int us)
{
	if ((duration > (us - DURATION_DELTA)) && (duration < us + DURATION_DELTA))
		return 1;
	else
		return 0;
}

/*
 * 返回值: 0-得到数据, 1-得到重复码, -1 : 失败
 */
int irda_nec_read(int *address, int *data)
{
	irda_raw_event event;	
	int i;
	unsigned int val = 0;
	unsigned char byte[4];

	while (1)
	{
		if (0 == ir_event_get(&event))
		{
			/* 解析数据 */
			/* 1. 判断是否为9MS的低脉冲 */
			if (event.pol == 0 && \
				duration_in_margin(event.duration, DURATION_HEAD_LOW))
			{
				/* 进行后续判断 */
				/* 2. 读下一个高脉冲数据 */
				if (0 == ir_event_get_timeout(&event, 10000))
				{
					/* 3. 判断它是否4.5ms的高脉冲
					 *    或者 2.25ms的高脉冲
					 */
					if (event.pol == 1 && \
						duration_in_margin(event.duration, DURATION_HEAD_HIGH))
					{
						/* 4.5ms的高脉冲 */
						/* 4. 重复解析32位数据 */
						for (i = 0; i < 32; i++)
						{
							/* 5. 读0.56ms的低脉冲 */
							if (0 == ir_event_get_timeout(&event, 10000))
							{
								if (event.pol == 0 && \
									duration_in_margin(event.duration, DURATION_DATA_LOW))
								{
									/* 6. 读下一个数据, 判断它是 0.56ms/1.68ms的高脉冲 */
									if (0 == ir_event_get_timeout(&event, 10000))
									{
										if (event.pol == 1 && \
											duration_in_margin(event.duration, DURATION_DATA1_HIGH))
										{
											/* 得到了bit 1 */
											val |= (1<<i);
										}
										else if (event.pol == 1 && \
												duration_in_margin(event.duration, DURATION_DATA0_HIGH))
										{
											/* 得到了bit 0 */
										}
										else
										{
											printf("%s %d\n\r", __FUNCTION__, __LINE__);
											return -1;
										}
									}
									else
									{
										printf("%s %d\n\r", __FUNCTION__, __LINE__);
										return -1;
									}
								}
								else
								{
									printf("%s %d\n\r", __FUNCTION__, __LINE__);
									return -1;
								}
							}
							else
							{
								printf("%s %d\n\r", __FUNCTION__, __LINE__);
								return -1;
							}
						}

						/* 7. 得到了32位数据, 判断数据是否正确 */
						byte[0] = val & 0xff;
						byte[1] = (val>>8) & 0xff;
						byte[2] = (val>>16) & 0xff;
						byte[3] = (val>>24) & 0xff;

						//printf("get data: %x %x %x %x\n\r", byte[0], byte[1], byte[2], byte[3]);
						byte[1] = ~byte[1];
						byte[3] = ~byte[3];
						
						if (byte[0] != byte[1])
						{
							/* 有些遥控器不完全遵守NEC规范 */
							//printf("%s %d\n\r", __FUNCTION__, __LINE__);
							//return -1;
						}
						if (byte[2] != byte[3])
						{
							printf("%s %d\n\r", __FUNCTION__, __LINE__);
							return -1;
						}
						*address = byte[0];
						*data    = byte[2];
						return 0;
						
					}
					else if (event.pol == 1 && \
						duration_in_margin(event.duration, DURATION_REPEAT_HIGH))
					{
						/* 2.25ms的高脉冲 */
						return 1;  /* 重复码 */
					}
					else
					{
						printf("%s %d\n\r", __FUNCTION__, __LINE__);
						return -1;  /* 错误 */
					}
				}
			}
			else
			{
				//printf("%s %d\n\r", __FUNCTION__, __LINE__);
				return -1; /* 有效数据未开始 */
			}
		}
	}
}

void irda_nec_test(void)
{
	int address;
	int data;
	int ret;
	
	irda_init();

	while (1)
	{
		ret = irda_nec_read(&address, &data);
		if (ret == 0)
		{	/*输出地址和数据*/
			printf("irda_nec_read: address = 0x%x, data = 0x%x\n\r", address, data);
		}
		else if (ret == 1)
		{
			printf("irda_nec_read: repeat code\n\r");
		}
	}
}