4、传感器数据协议解析(续):风速风向传感器协议解析、雨量传感器(翻斗式)脉冲计数、光照强度传感器(BH1750)I2C通信
好,咱们接着聊传感器协议解析。上一章我们把温湿度和气压传感器讲透了,这一章轮到剩下的三个硬骨头:风速风向、雨量计和光照强度。这三个传感器,说实话,协议风格完全不同。风速风向靠的是模拟量或脉冲,雨量计靠的是中断计数,BH1750则是标准的I2C从设备。我当年第一次把这三个传感器凑到一起调试时,差点被它们的“个性”搞疯掉。嗯,咱们一个一个来。
4.1 风速风向传感器:模拟量与脉冲的博弈
风速风向传感器,在农业气象站里最常见的是“风杯+风向标”的组合。风杯转得快慢代表风速,风向标指向哪里就是风向。但问题来了——怎么把机械转动变成电信号?
市面上主流方案有两种:
- 模拟电压输出:风速对应0-5V或0-10V,风向对应0-5V(0°-360°)。
- 脉冲频率输出:风速越高,脉冲频率越快。风向则用格雷码或电压分段表示。
我个人习惯用脉冲式风速传感器,因为抗干扰能力强,而且可以直接用单片机的定时器捕获。模拟量的话,还得加ADC,布线也麻烦。
4.1.1 风速脉冲频率解析
风速传感器的脉冲输出,说白了就是一个方波。风杯每转一圈,输出一个或几个脉冲。厂家会给一个公式,比如:
风速(m/s) = 频率(Hz) × 0.0875
这个系数每个厂家不一样,买传感器时一定要看 datasheet。我曾经在一个项目里吃过亏——用了A厂的风速计,却套了B厂的公式,结果风速数据偏了30%。后来查了两天,才发现是系数搞错了。
在STM32上,我一般用定时器的输入捕获模式来测频率。代码大概长这样:
// 定时器输入捕获初始化
void TIM_IC_Init(void) {
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0F; // 滤波,防止毛刺
TIM_ICInit(TIM2, &TIM_ICInitStructure);
}
// 中断服务函数中计算频率
uint32_t CapturedValue = 0;
uint32_t Frequency = 0;
void TIM2_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET) {
CapturedValue = TIM_GetCapture1(TIM2);
// 假设定时器时钟72MHz,预分频72-1,则计数频率1MHz
Frequency = 1000000 / CapturedValue; // 单位Hz
TIM_ClearITPendingBit(TIM2, TIM_IT_CC1);
}
}
这里有个坑:如果风速很低,脉冲间隔很长,CapturedValue可能会溢出。我建议加一个超时判断,比如超过1秒没捕获到脉冲,就认为风速为0。
4.1.2 风向模拟量解析
风向传感器通常输出0-5V模拟电压,对应0-360°。但注意,不是线性的!很多便宜的风向传感器用的是“电位器+电阻分压”,电压和角度之间是近似线性,但会有死区。我建议拿到传感器后先做一次标定:
| 电压(V) | 角度(°) |
|---|---|
| 0.5 | 0 (北) |
| 1.5 | 90 (东) |
| 2.5 | 180 (南) |
| 3.5 | 270 (西) |
| 4.5 | 360 (北) |
实际使用时,用ADC采集电压,然后线性插值。但要注意:0°和360°是同一个点,代码里要做环形处理。我一般这样写:
float voltage = Get_ADC_Value() * 3.3f / 4096.0f; // 假设12位ADC,参考电压3.3V
float angle = (voltage - 0.5f) * (360.0f / 4.0f); // 0.5V对应0°,4.5V对应360°
if (angle < 0) angle = 0;
if (angle > 360) angle = 360;
你想想看,如果直接用这个角度去算平均风向,0°和360°会算出180°来,那就闹笑话了。所以风向平均要用矢量平均法,不能直接算术平均。
4.2 雨量传感器(翻斗式):脉冲计数的艺术
翻斗式雨量计,原理很简单:雨水接满一个小斗,斗翻一次,倒掉水,同时触发一个干簧管或霍尔开关,输出一个脉冲。每个脉冲代表0.1mm或0.2mm的降雨量。
但简单归简单,实际用起来全是细节。我来说说几个关键点。
4.2.1 脉冲计数与防抖
翻斗翻转时,干簧管会抖动,产生多个脉冲。如果不做防抖,一次翻斗可能被计成3-5次。我建议用硬件滤波+软件消抖:
// 外部中断服务函数
void EXTI0_IRQHandler(void) {
if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
// 软件消抖:记录当前时间
static uint32_t last_time = 0;
uint32_t now = HAL_GetTick();
if (now - last_time > 50) { // 50ms消抖
rain_pulse_count++;
last_time = now;
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
50ms的消抖时间够吗?我实测过,大部分翻斗式雨量计的抖动时间在10-30ms之间,50ms是安全的。但如果你用的是高灵敏度干簧管,可能需要调到100ms。
4.2.2 累计雨量与实时雨量
雨量数据有两种:累计雨量和时段雨量。累计雨量就是脉冲数乘以分辨率,比如100个脉冲×0.2mm=20mm。时段雨量则是每5分钟或1小时统计一次。
我一般这样设计数据结构:
typedef struct {
uint32_t total_pulses; // 总脉冲数
float total_rainfall; // 累计雨量(mm)
uint32_t hourly_pulses; // 当前小时脉冲数
float hourly_rainfall; // 小时雨量(mm)
uint32_t last_hour; // 上次统计的小时
} RainGauge_t;
每小时整点时,把hourly_pulses清零,同时把数据存入历史记录。注意:清零操作要关中断,防止丢失脉冲。
4.3 光照强度传感器(BH1750):I2C通信实战
BH1750是数字光照传感器,用I2C接口。它直接输出勒克斯(lux)值,范围1-65535 lux。说实话,用起来比模拟光敏电阻省心多了——不用标定,不用ADC,直接读寄存器就行。
4.3.1 I2C地址与初始化
BH1750的I2C地址由ADDR引脚决定:
- ADDR接GND:地址0x23(7位)
- ADDR接VCC:地址0x5C
我习惯接GND,因为0x23好记。初始化时,要发送一个测量指令:
#define BH1750_ADDR 0x23
#define BH1750_POWER_ON 0x01
#define BH1750_CONT_H_RES_MODE 0x10 // 连续高分辨率模式
void BH1750_Init(void) {
I2C_Start();
I2C_SendByte(BH1750_ADDR << 1); // 写地址
I2C_SendByte(BH1750_POWER_ON); // 上电
I2C_Stop();
delay_ms(10);
I2C_Start();
I2C_SendByte(BH1750_ADDR << 1);
I2C_SendByte(BH1750_CONT_H_RES_MODE); // 设置连续高分辨率模式
I2C_Stop();
delay_ms(180); // 等待第一次测量完成
}
这里有个细节:连续高分辨率模式的测量时间是120ms,但第一次测量可能需要180ms。我建议初始化后等200ms再读数据,比较保险。
4.3.2 读取光照值
读取光照值需要先发读指令,然后接收2个字节:
uint16_t BH1750_ReadLight(void) {
uint8_t buf[2];
I2C_Start();
I2C_SendByte(BH1750_ADDR << 1 | 0x01); // 读地址
buf[0] = I2C_ReadByte(ACK);
buf[1] = I2C_ReadByte(NACK);
I2C_Stop();
uint16_t raw = (buf[0] << 8) | buf[1];
float lux = raw / 1.2f; // 高分辨率模式下,1 count = 1.2 lux
return (uint16_t)lux;
}
为什么除以1.2?因为BH1750的原始值除以1.2才是勒克斯。这个系数在datasheet里有,但很多人会忽略。我记得第一次用BH1750时,直接拿原始值当lux用,结果大白天读出来才几百,还以为传感器坏了。
4.3.3 I2C通信的常见坑
I2C通信看着简单,但实际调试时容易出问题。我总结几个常见坑:
- 上拉电阻:SCL和SDA必须接上拉电阻,一般4.7kΩ。如果不接,通信会时好时坏。
- 时序冲突:如果总线上有多个I2C设备,地址不能冲突。BH1750只有两个地址可选,如果和别的设备冲突,就得换传感器。
- 时钟延展:BH1750在测量期间会拉低SCL,这叫时钟延展。有些MCU的I2C外设不支持时钟延展,需要用软件模拟I2C。
4.4 三种传感器的数据融合
好了,三个传感器都讲完了。但实际项目中,它们的数据要融合到一起,才能形成完整的气象数据包。我一般这样设计数据帧:
typedef struct {
float wind_speed; // 风速,单位m/s
float wind_direction; // 风向,单位°
float rainfall; // 累计雨量,单位mm
float rainfall_rate; // 雨强,单位mm/h
uint16_t light_intensity; // 光照强度,单位lux
uint32_t timestamp; // 时间戳
} WeatherData_t;
采集周期我设为5秒一次。风速和风向用5秒内的平均值,雨量用累计值,光照用瞬时值。然后每5分钟上传一次到服务器。
你可能会问:为什么不上传更频繁?因为农业气象站通常用太阳能供电,上传数据太频繁会耗电。5分钟一次,既保证了数据连续性,又省电。我在项目里实测过,5秒采集、5分钟上传,一块12V 20Ah的电池能撑7天阴天。
嗯,这一章内容不少。从风速的脉冲捕获,到风向的模拟量处理,再到雨量的防抖计数,最后是BH1750的I2C通信。每个传感器都有自己的脾气,但只要摸透了协议,写起代码来就顺了。下一章我们开始讲数据存储和上传,到时候这些传感器数据就要真正派上用场了。