第022课 传感器
目录
第001节_光敏电阻的使用
这节课我们开始讲的传感器,有光敏电阻、DH11温湿度传感器、DS18B20温度传感器、HS0038红外接收器。
首先介绍光敏电阻传感器。
光敏电阻有一个特点,就是它的阻值随光照强度变化而变化,
看一下它的原理图,R5是一个普通电阻,LAS1是光敏电阻,它们串联组成一个分压电路,
LAS1阻值变化,将会导致中间RES_AO测得的电压发生变化。
这个电路图有点绕,画一个示意图如下:
使用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。我们可以仿照之前的按键中断来编写本次的中断。
先分析一下中断,如图:
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):
/* 尽量少调用函数 */
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温湿度传感器的使用,首先查看芯片手册,里面的典型应用电路如下:
MCU通过一条数据线与DH11连接,MCU通过这条线发命令给DH11,DH11再通过这条线把数据发送给MCU。
因此,温湿度模块的核心就是 MCU发给DH11的命令格式和DH11返回的数据格式。
再来先简单看一下通讯的时序:
灰色这条线是由MCU驱动控制的,浅色的部分是由DH11驱动控制的。
首先MCU发送一个开始信号,这个开始信号是一个低脉冲,然后再拉高。
然后,DH11拉低,做出一个响应信号,再拉高。
接着就是DH11返回的数据。
这些数据一共有40bit,高位先出。
数据格式:8bit湿度整数数据+8bit湿度小数数据+8bi温度整数数据+8bit温度小数数据+8bit校验和
且当前小数部分用于以后扩展,现读出为零.
数据传送正确时校验和数据等于“8bit湿度整数数据+8bit湿度小数数据+8bi温度整数数据+8bit温度小数数据”所得结果的末8位。
DH11的难点是前面所说的时序脉冲,需要满足一定的时长.比如开始信号:
MCU必须先拉低至少18ms,然后再拉高20-40us,DH11再拉低80us以响应,最后再拉高80us.
接下来就是传输数据,我们的目的就是读到温湿度的数据,这些数据由DH11提供,那它怎么传回这些数据,怎么表示0和1呢?
可以看到,不管是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;
}