匿名
未登录
登录
百问网嵌入式Linux wiki
搜索
查看“第022课 传感器”的源代码
来自百问网嵌入式Linux wiki
名字空间
页面
讨论
更多
更多
页面选项
Read
查看源代码
历史
←
第022课 传感器
因为以下原因,您没有权限编辑本页:
您所请求的操作仅限于该用户组的用户使用:
用户
您可以查看与复制此页面的源代码。
=第001节_光敏电阻的使用= 这节课我们开始讲的传感器,有光敏电阻、DH11温湿度传感器、DS18B20温度传感器、HS0038红外接收器。 首先介绍光敏电阻传感器。 光敏电阻有一个特点,就是它的阻值随光照强度变化而变化, 看一下它的原理图,R5是一个普通电阻,LAS1是光敏电阻,它们串联组成一个分压电路, LAS1阻值变化,将会导致中间RES_AO测得的电压发生变化。 [[File:chapter22_lesson1_001.png|800px]] 这个电路图有点绕,画一个示意图如下: [[File:chapter22_lesson1_002.png|800px]] 使用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: <syntaxhighlight lang="c" > 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; } </syntaxhighlight> 后面的adc_read_ain0只能读取AIN0的数据,现在修改一下,传入个通道参数,就能读取对应通道的ADC值: <syntaxhighlight lang="c" > int adc_read(int channel) { adc_init(channel); /* 启动ADC */ ADCCON |= (1<<0); while (!(ADCCON & (1<<15))); /* 等待ADC结束 */ return ADCDAT0 & 0x3ff; } </syntaxhighlight> 修改了这两个函数,原来的adc_test函数里调用的adc读取函数也要对应进行修改。 参考这个adc_test,编写photoresistor_test函数。 需要修改的内容并不多,首先是修改adc_read参数,将通道0改为通道1。 然后我们想同时再把AIN0上的滑动电阻对应的电压也读出来,因此再做一次ADC0读取的操作。 <syntaxhighlight lang="c" > 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(); } </syntaxhighlight> 以上就完成了我们的第一个目标。 现在开始做第二个目标,注册中断放在interrupt.c里实现。 硬件上RES_DO是EINT15。我们可以仿照之前的按键中断来编写本次的中断。 先分析一下中断,如图: [[File:chapter22_lesson1_003.png|800px]] GPG7作为中断引脚,它会先经过外部中断EINTMASK寄存器,才能进入到中断控制器。 所以,需要做以下操作: *①首先初始化: :: a.GPG7配置为中断引脚; :: b.设置中端触发方式:双边沿触发; :: c.设置EINTMASK使能中断; *②中断处理: :: a.分辨:读EINTPEND; :: b.读GPG7; 在原来的key_eint_init函数里配置GPG7为中断引脚: <syntaxhighlight lang="c" > /* 配置GPG7为中断引脚, 用于光敏电阻 */ GPGCON &= ~((3<<14)); GPGCON |= ((2<<14)); </syntaxhighlight> 然后设置中端触发方式:双边沿触发 <syntaxhighlight lang="c" > /* 设置中断触发方式: 双边沿触发 */ EXTINT0 |= (7<<0) | (7<<8); /* S2,S3 */ EXTINT1 |= (7<<12); /* S4 */ EXTINT2 |= (7<<12); /* S5 */ </syntaxhighlight> 最后再使能中断: <syntaxhighlight lang="c" > /* 使能中断GPG7/EINT15, 用于光敏电阻 */ EINTMASK &= ~((1<<15)); </syntaxhighlight> 修改key_eint_irq中断处理函数,先判断是哪个中断产生,再读取电平,打印。 <syntaxhighlight lang="c" > else if (val & (1<<15)) /* eint15, 用于光敏电阻 */ { if (val2 & (1<<7)) { printf("\n\rphotoresistor dark!\n\r"); } else { printf("\n\rphotoresistor light!\n\r"); } } </syntaxhighlight> 至此,代码就写完了,最后还要修改对应的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。 修改对应的寄存器: <syntaxhighlight lang="c" > TCFG0 = 4; /* Prescaler 0 = 4, 用于timer0,1 */ TCFG1 &= ~0xf; /* MUX0 : 1/2 */ /* 设置TIMER0的初值 */ TCNTB0 = 50000; /* 10Ms中断一次 */ </syntaxhighlight> 我们先写一个us延时的函数,然后ms延时就调用us即可。 因此,us延时函数里,尽量少调用函数。 假如现在要延时nus,我们先将n*5,得到nus对应的“计数时钟数”。 然后如果传入“计数时钟周期”如果大于0,则一直计算过去了多少个“计数时钟数”,与传入的“计数时钟数”相减,直到为零,退出循环,也就实现了延时nus。 怎样计算过去了多少个“计算周期”呢? 自然是当前的值,减去一开始进入函数的值。 但还有一种情况是定时器里的计数记到0时,会自动变成5000,计数计数,这时候,计算方式就变成了pre+(5000-cur): [[File:chapter22_lesson2_001.png|700px]] <syntaxhighlight lang="c" > /* 尽量少调用函数 */ 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; } } </syntaxhighlight> 然后时ms延时函数: <syntaxhighlight lang="c" > void mdelay(int m) { udelay(m*1000); } </syntaxhighlight> 我们可以写一个测试函数,简单的测试下是否可用,测试函数隔1分钟进行打印一下。 如果us不准的话,放大至s,会有比较大的偏差,这样可以进行粗略的检测,精确检测可以使用示波器等工具。 <syntaxhighlight lang="c" > void hrtimer_test(void) { int cnt = 0; while (1) { printf("delay one min: "); mdelay(60000); /* 延时1分钟 */ printf("%d\n\r", ++cnt); } } </syntaxhighlight> 前面延时里的计算还是比较耗费时间的,因此,我们尽量提高CPU的运行时钟,并且 将尽可能的启动icache、dcache和mmu。 此外,如果延时过程中,发生了中断,如果中断比较耗时的话,就会导致延时可能出现不准确,所以,我们可以延时之前关中断, 延时之后开中断; 课后作业: * a. 禁止icache, 禁止mmu, 修改lds, 测试延时函数是否还准确; * b. 测试延时之前关中断, 延时之后开中断; =第003节_DHT11温湿度传感器的使用= 这节课开始讲解DH11温湿度传感器的使用,首先查看芯片手册,里面的典型应用电路如下: [[File:chapter22_lesson3_001.png|700px]] MCU通过一条数据线与DH11连接,MCU通过这条线发命令给DH11,DH11再通过这条线把数据发送给MCU。 因此,温湿度模块的核心就是 MCU发给DH11的命令格式和DH11返回的数据格式。 再来先简单看一下通讯的时序: [[File:chapter22_lesson3_002.png|700px]] 灰色这条线是由MCU驱动控制的,浅色的部分是由DH11驱动控制的。 首先MCU发送一个开始信号,这个开始信号是一个低脉冲,然后再拉高。 然后,DH11拉低,做出一个响应信号,再拉高。 接着就是DH11返回的数据。 这些数据一共有40bit,高位先出。 数据格式:8bit湿度整数数据+8bit湿度小数数据+8bi温度整数数据+8bit温度小数数据+8bit校验和 且当前小数部分用于以后扩展,现读出为零. 数据传送正确时校验和数据等于“8bit湿度整数数据+8bit湿度小数数据+8bi温度整数数据+8bit温度小数数据”所得结果的末8位。 DH11的难点是前面所说的时序脉冲,需要满足一定的时长.比如开始信号: [[File:chapter22_lesson3_003.png|700px]] MCU必须先拉低至少18ms,然后再拉高20-40us,DH11再拉低80us以响应,最后再拉高80us. 接下来就是传输数据,我们的目的就是读到温湿度的数据,这些数据由DH11提供,那它怎么传回这些数据,怎么表示0和1呢? [[File:chapter22_lesson3_004.png|700px]] [[File:chapter22_lesson3_005.png|700px]] 可以看到,不管是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模式,实现输出、输入引脚的功能: <syntaxhighlight lang="c" > 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); } </syntaxhighlight> 再设置输出电平或读取引脚数据: <syntaxhighlight lang="c" > 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; } </syntaxhighlight> 再来实现DHT11的读操作。 在芯片手册里介绍说,DH11传感器上电后,要等待1s,以越过不稳定状态,在此期间无需发送任何指令。 因此首先写一个初始化函数,跳过这个不稳定状态: <syntaxhighlight lang="c" > void dht11_init(void) { dht11_data_cfg_as_output(); dht11_data_set(1); mdelay(2000); } </syntaxhighlight> 根据start时序要求,编写程序,维持一个大于18ms的低电平,然后释放引脚,及设置为输入引脚即可。 因为该引脚接有上拉电阻,一旦MCU设置为输入,引脚电平将有上拉电阻决定。 <syntaxhighlight lang="c" > static void dht11_start(void) { dht11_data_set(0); mdelay(20); dht11_data_cfg_as_input(); } </syntaxhighlight> 然后等待40us以上,再去读取引脚电平,判断是否被拉低,以确定DH11给了响应。 <syntaxhighlight lang="c" > static int dht11_wait_ack(void) { udelay(60); return dht11_data_get(); } </syntaxhighlight> 再写个延时函数,用于时序中的,等待响应信号结束: <syntaxhighlight lang="c" > 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 */ } </syntaxhighlight> 后面的数据会有五个字节组成,这里先写出读取一个字节,每个字节要读取8位。 先等待直到高电平,过滤到共同的50us延时,然后延时28us以上,再读取引脚电平, 如果引脚电平是1,则数据是1,反之是0。 然后再直到低电平的到来,循环8次,完成一个字节数据的读取。 <syntaxhighlight lang="c" > 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; } </syntaxhighlight> =第004节_DS18B20温度传感器介绍= 比DHT11温湿度传感器精度高很多 [[File: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读温度,读数据 每次操作,都要重重上述过程 内部框图 [[File:chapter22_lesson4_002.png|700px]] TEMPERATURE SENSOR温度ADC SCRATCHPAD实际上是一个9 byte的内存 9byte的说明如下图所示 [[File:chapter22_lesson4_003.png|700px]] 温度值会保存在9byte内存中的 BYTE0 和 BYTE1 也就是当我们发出一个温度值的命令之后,还需要发送一个读内存的命令才能把温度值读取出来 最后一位是CRC校验码,通过前8位的数据和最后一位的校验码比较 64位数据中有8位是校验码, 怎么采样温度? *1 初始化 *2 ROM命令 *3 FunctionCommand 设置某些值,比如转换温度 *4等待完成 *5 init *6 R om cmd *7 Function Command 读RAM中的值 [[File: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命令和功能命令整理成一个表格 {| class="wikitable" |- ! 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 || 警报搜索 执行此命令后,只有温度超过设定值上限或下限的芯片才会做出响应 |} {| class="wikitable" |- !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 初始化时序 [[File:chapter22_lesson4_005.png|700px]] 一开始是高电平,想要开始传输信号,必须要拉低至少480us释放总线 经过15~60us之后 DS18B20会把这条线拉低60~240us 2 怎么发数据,怎么发出1bit 怎么发出bit0 怎么发出bit1 写数据时序 [[File:chapter22_lesson4_006.png|700px]] 不论是写0还是写1时序都是大于60us 写0拉低总线维持60us以上 写1时,信号线拉低1us时间,提醒要写数据了,让后回高,写1位之间的时间间隔1us [[File:chapter22_lesson4_007.png|700px]] 读数据时序 也是由主机发起 提醒脉冲大于1us,主机马上释放总线 在15us之内读信号,一个读周期至少是60us,每位的间隔也是1us DS18B20提供了编程图 [[File:chapter22_lesson4_008.png|700px]] 供电方式 [[File:chapter22_lesson4_009.png|700px]] 参考资料: 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温度传感器编程= 设置精度相关的位 创建ds18b20目录 查看使用那个引脚 使用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节_红外线遥控协议简介及编程思路= 本节开始讲解红外遥控器信号的接收和解码,视频分为三部分,每一部分都专注做一件事情,让每节视频更短一点。 红外遥控器的操作比前面的温度、温湿度传感器都要简单。 首先看一下原理图上的红外遥控接收器: [[File:chapter22_lesson6_001.png|700px]] 我们用遥控器对它按动的时候,它就可以接收到红外信号,然后把红外信号转换成电平信号,通过IRD这根线,传给SOC。 整个传输,只涉及单向传输,由HS0038向主芯片传送。 因此,我们只需要编写程序,从IRD上获取数据即可,在这之前,我们需要先了解下数据是怎么表示的,也就是传输的红外数据的格式。 红外协议有:NEC、SONY、RC5、RC6等,常用的就是NEC格式,因此我们主要对NEC进行讲解。 可以参考这个文章,直观的了解下NEC格式波形的样子: https://www.cnblogs.com/openusb/archive/2010/01/07/1641357.html 在分析文章中的波形之前,我们先想象一下怎么在一条数据线上传输信号。 开始传输数据之前,一般都会发出一个start起始信号,通知对方我开始传输数据了,后面就是每一位每一位的数据。 NEC协议的开始是一段引导码: [[File:chapter22_lesson6_002.png|700px]] 这个引导码由一个9ms的低脉冲加上一个4.5ms的高脉冲组成,它用来通知接收方我要开始传输数据了。 [[File:chapter22_lesson6_003.png|700px]] 然后接着的是数据,数据由4字节组成:地址、地址(取反)、数据、数据(取反),这些取反是用来校验用的。 地址是指遥控器的ID,每一类遥控器的ID都不一样,数据就是遥控器上的不同按键。 从前面的图可以知道,NEC每次要发32位的数据,每一位用什么来表示0和1呢? [[File:chapter22_lesson6_004.png|700px]] 数据1和01,开始都是0.56ms的低脉冲,对于数据1,后面的高脉冲比较长,对于数据0,后面的高脉冲比较短。 可以看出,红外遥控器的数据表示方法是比较简单的。 我们长按一个按键,第一次按的时候,他会发出引导码,地址,地址取反,数据,数据取反。 接着由于长按,遥控器会发送一个不一样的引导码,这个引导码由9ms的低脉冲,2.25ms的高脉冲组成,表示现在按的还是上次一样的按键,然后再一直是引导码(重复),直到松开。 [[File:chapter22_lesson6_005.png|700px]] 在后面的调试中,发现以上并不是NEC协议的全部,打开bing国际版搜索“ir nec protocal”,得到一篇官方文章:http://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol 里面的内容和前面文章基本一致,但这个更详细,发现每次数据传输完还有一个0.5625ms的低脉冲表示数据传输结束。 对于引导码(重复)也一样,也有一个0.5625ms的低脉冲表示传输结束。 大部分文章都漏掉了结束的低脉冲。 NEC协议里有很多时间,这些时间有一个有趣的现象,把所有时间里面最小的0.53ms看作基本脉冲宽度,假设用t表示,那么其它所有时间都是t的倍数: [[File:chapter22_lesson6_006.png|700px]] 我们可以看到对于所有的时间,最小的单位都是0.56ms,这个时间对人来说是非常短的,但对嵌入式系统它是非常非常长的了,足够我们做很多事情了,那么我们可以使用中断来处理这些数据。 并且对于红外遥控器来说,我们根本不知道用户什么时候按下遥控器,使用轮询的方式特别耗资源,因此直接使用中断来处理。 使用官方文档的时序图: [[File:chapter22_lesson6_007.png|700px]] 图中的脉冲方向正好相反,绿色表示低脉冲,白色表示高脉冲。 涉及内容: * ①中断引脚设置为双边缘触发,在每一个脉冲变化的地方都会产生中断; * ②发生中断时,计算当前中断与上次中断之间的时间差,就得到脉冲宽度,放入buffer,同时还要记录引脚极性; * ③主循环从buffer取出数据,并解析时序; 我们可以估算下,每按下一次遥控器,会产生多少中断,2+32*2+1=67次。 中断发生时,将数据放入buffer,主函数从buffer取出数据,用什么数据结构来实现数据的存取? 最好的方式就是环形缓冲区,所谓环形缓冲区就是一边存储数据一边读取数据,下节课再详细讲解。 编程要点: *①中断 *②系统时间 *③环形缓冲区 *④NEC解析 =第007节_前期编程_系统时间与环型缓冲区= 这节课实现两个小功能:'''系统时间'''和'''环形缓冲区'''。 在上一课的基础上添加代码,打开timer.c,前面的设置的定时器,每10ms产生一次中断,这里定义一个全局变量,来记录产生次数, <syntaxhighlight lang="c" > static unsigned int g_system_time_10ms_cnt = 0; </syntaxhighlight> 这里的类型选择使用<code>unsigned int</code>类型,2^32*10/1000/3600/24=497天,也就是说如果运行497天后,计数溢出,将会导致一些问题。 改为<code>unsigned long long</code>类型的话,2^64*10/1000/3600/24/365=5849424173年,这个时间就不怕溢出了,因此这里计数变量的类型为<code>unsigned long long</code>: <syntaxhighlight lang="c" > static unsigned long long g_system_time_10ms_cnt = 0; </syntaxhighlight> 在定时器中断timer_irq()函数里面让这个计数值每次加1: <syntaxhighlight lang="c" > g_system_time_10ms_cnt++; </syntaxhighlight> 以后我们就可以读取这个计数值,知道系统时间。 现在开始编写获取系统时间的函数,精度要求是us级别的,读取TCNTO0的值,再加上g_system_time_10ms_cnt的计数: <syntaxhighlight lang="c" > unsigned long long get_system_time_us(void) { unsigned long long us = (50000 - TCNTO0)/5; return g_system_time_10ms_cnt * 10 * 1000 + us; } </syntaxhighlight> 通过这个函数就知道上电到之后任何一个时刻,过去了多久。 再写一个函数计算两段时间之间的差值: <syntaxhighlight lang="c" > unsigned int delta_time_us(unsigned long long pre, unsigned long long now) { return (now - pre); } </syntaxhighlight> 到此,系统时间相关函数就完成了。 首先介绍一下环形缓冲区,假设有一个数组<code>char Buf[6]</code>,它的结构如下: [[File:chapter22_lesson7_001.jpg|700px]] 定义一个读指针<code>r=0</code>,一个写指针<code>w=0</code>。 * 写数据: <code>buf[w] = val;</code> <code>w = (w+1)%LEN = (w+1)%6;</code> * 读数据: <code>val = buf(r);</code> <code>r = (r+1)%LEN;</code> 如何判断buf是空: <code>r == w; //为空</code> 如何判断buf是满: <code>(w+1)%LEN == r; //为满</code> 读写指针,每到达最后面,就从0开始,就像一个圆环一样,因此得名环形缓冲区。 对于我们红外数据,保存的数据并不是char,而是一个结构体,里面含有脉冲宽度,引脚极性等。 在sensors文件下创建一个irda文件夹,里面创建irda_raw.h和circle_buffer.c,在irda_raw.h里定义一个数据结构体,包含极性和脉冲宽度: <syntaxhighlight lang="c" > #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 */ </syntaxhighlight> 然后在circle_buffer.c实现环形缓冲区。 先定义个irda_raw_event类型的g_events[]数组,这里大小设置为1024, 之前介绍过,每传一次irda,至少会传67次数据,因此这个buf要至少大于67行,再定义两个读写指针位置。 <syntaxhighlight lang="c" > static irda_raw_event g_events[1024]; static int g_r = 0; static int g_w = 0; </syntaxhighlight> 判断buf是否是空的函数: <syntaxhighlight lang="c" > static int is_ir_event_buf_empty(void) { return g_r = g_w; } </syntaxhighlight> 判断buf是否是满的函数: <syntaxhighlight lang="c" > static int is_ir_event_buf_full(void) { return NEXT_PLACE(g_w) == g_r; } </syntaxhighlight> 其中,(w+1)%LEN使用宏NEXT_PLACE(i)代替,宏的定义如下: <syntaxhighlight lang="c" > #define NEXT_PLACE(i) ((i+1)&0x3FF) </syntaxhighlight> %的操作使用位&操作实现一样的效果。 然后是把数据放入缓冲区: <syntaxhighlight lang="c" > 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; } </syntaxhighlight> 先判断的缓冲区是否已满,没满的话就在写的位置放入数据,然后写位置再移动到下一个。 最后是读数据: <syntaxhighlight lang="c" > 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; } </syntaxhighlight> 先判断的缓冲区是否是空,没空的话就在读的位置读出数据,然后读位置移到到下一个。 修改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 编译执行 一开始时间很长是因为一上电的时候高电平,值打印的特别大,说明有问题 irda中断函数中没有更新上一次的时间 /*更新时间*/ g_last_time = cur; 再次更新 时间和数据格式符合时序要求 =第009节_HS0038红外线接收器的编程_解析数据= 解析NEC格式的数据 创建irda_nec.c NEC解析数据 32 对于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"); } } } ='''《《所有章节目录》》'''= <categorytree mode=all background*color:white;">ARM裸机加强版</categorytree> [[Category:ARM裸机加强版 ]] [[Category:Jz2440 ]] [[Category:Irda红外传感器]] [[Category:ds18b20温湿度传感器]] [[Category:光敏电阻传感器]]
返回至
第022课 传感器
。
导航
导航
WIKI首页
官方店铺
资料下载
交流社区
所有页面
所有产品
MPU-Linux开发板
MCU-单片机开发板
Linux开发系列视频
单片机开发系列视频
所有模块配件
Wiki工具
Wiki工具
特殊页面
页面工具
页面工具
用户页面工具
更多
链入页面
相关更改
页面信息
页面日志