简介:一份基于TI CC2530与光敏传感器的无线光照采集工程,面向物联网、嵌入式及Zigbee无线传感网初学者,用于掌握传感器驱动、数据采集与无线组网技能。项目以IAR为开发环境,结合ZStack-2.5.1a协议栈,完整实现光敏电阻信号调理、A/D转换、数字滤波及Zigbee组网通信,覆盖初始化配置、参考电压与采样通道选择、定时触发转换,再到终端节点发送、路由器/协调器接收的完整数据链路。压缩包共1064个文件,约14.66MB,包含182个C源码、170个头文件,以及众多IAR工程文件(ewp/ewd)、编译中间文件和hex固件,可直接导入工程进行编译烧录与二次开发。工程内附路由与协调器代码、Zigbee协议栈配置及调试辅助文件,有助于理解网络建立、节点入网、数据路由与透传等关键环节。目前已有4530人学习下载,代码结构清晰、注释明确,是从外设驱动到无线应用的一体化参考范例。
1. 项目定位:CC2530 光敏传感器代码能干什么
假设你手头有一块基于 CC2530 的 ZigBee 节点板,想把它做成一个能感知环境光照度的传感器节点,却卡在 ADC 采样不稳定、数据怎么塞进 ZigBee 帧、协调器收不到数据这一连串问题上。这份「基于CC2530的光敏传感器代码」就是用来解决这类落地问题的:它把光敏电阻或光敏二极管模块接到 CC2530 的 ADC 通道,完成电压采集、滤波、阈值判断,再通过 ZigBee 协议栈把光照数据周期性地上报给协调器。对于做智能家居、农业大棚光照监测、实验室环境记录的同学来说,这套代码可以直接移植到自己的节点上,改几个引脚和参数就能跑起来,不用从零啃协议栈源码。
整个工程基于 TI 的 Z-Stack 协议栈,代码风格偏向实际项目中的裸机与协议栈混合写法,ADC 部分用寄存器操作,无线上报部分走标准端点与簇接口。它既不是那种只有main里点个灯的玩具例程,也不是把你困在某款开发板里的封闭工程,而是把「采集 - 处理 - 上报」这条链路完整打通了。对新手来说,照着烧录就能看到串口输出;对熟练的开发者来说,可以快速把光敏通道替换成其他模拟量传感器,或者把上报帧改成自己定义的格式。
最值得看的是中间那层数据处理:光敏电阻的模拟电压随光照变化,但直接读 ADC 结果往往抖动得让人怀疑人生,这套代码里做了多次采样均值滤波,并且给出了光照等级换算的参考公式,让你拿到的不只是原始 ADC 值,而是可以直接使用的环境光照量。接下来我会从硬件电路讲起,再把代码拆开看,最后带你把工程跑起来,并分享几个实际调试中容易踩的坑。
2. 硬件与协议栈:在 CC2530 上采集光照度的基础设计
2.1 CC2530 的 ADC 与光敏电阻分压电路
CC2530 内部集成了一个 12 位 ADC,支持 8 个输入通道,其中AIN0到AIN7对应 P0_0 到 P0_7 引脚。光敏传感器模块的常见接法是光敏电阻串联一个固定电阻,构成分压电路,把光照变化转化为电压变化,然后把这个电压送入 ADC 输入引脚。比如我常用的接法:光敏电阻一端接 VDD,另一端接 10kΩ 下拉电阻到 GND,中间抽头接到 P0_6,也就是AIN6通道。光照增强时,光敏电阻阻值下降,P0_6 电压升高;光照减弱时电压降低,ADC 读数线性反映这个电压。
分压电阻的取值很关键。如果选 10kΩ,在典型室内光照(几百 Lux)下,光敏电阻阻值大约在 5kΩ 到 20kΩ 之间变化,输出电压幅度能覆盖 ADC 量程的中段,不会出现低照度时读数被噪声吞掉、高照度时饱和的情况。若你的光照范围特别窄,可以把分压电阻换成 100kΩ 可调电阻,先跑一轮实测,把中间电压调到接近 1.5V,这样左右动态范围都留足余量。
在代码里,ADC 配置主要涉及几个寄存器:ADCCON1控制转换触发方式和采样结束标志,ADCCON2选择参考电压、采样位数和输入通道,ADCCON3在单次转换模式下指定起始通道。这套代码使用单次转换模式,参考电压选择AVDD5内部电源,12 位分辨率,启动方式为手动ADCCON1 |= 0x40。需要注意,CC2530 的 ADC 参考电压可选项包括内部 1.25V、AVDD5 和外部参考。对光敏电阻分压电路,一般分压输出电压不会超过 3V,所以选 AVDD5 足够,直接用读到的电压值反推光照时也更直观。
2.2 ZigBee 协议栈中传感器节点的角色划分
在一套典型的 ZigBee 网络中,有协调器(Coordinator)、路由器(Router)和终端设备(End Device)三类角色。光敏传感器节点通常作为终端设备或路由器节点存在。如果节点固定位置、需要经常休眠省电,那就配置成终端设备;如果节点还需要为其他节点转发数据,那就配置成路由器。CC2530 的 Z-Stack 工程里,这个角色是在f8wConfig.cfg文件中通过ZDO_COORDINATOR、RTR_NWK等宏定义来决定的。这套光敏传感器代码默认配置为终端设备,适合电池供电的独立节点。
协议栈中最核心的抽象是「端点(Endpoint)」和「簇(Cluster)」。每个设备可以有多个端点,每个端点对应一组应用描述。光敏传感器通常在端点 8 上注册一个输入簇,比如用自定义簇 ID0xFF01,或者直接使用标准 HA 配置里的「占用(Occupancy)」类簇。代码里发送数据时,会调用AF_DataRequest()函数,把待发送的光照数据打包成afDataConfirm_t格式,然后通过 APS 层发往协调器。理解这个流程不需要把整个 Z-Tack 源码读完,只需要知道:传感器采集的数字量必须放进一个afAckRequest_t结构体的cmd.Data数组里,指定目标地址和端点,协议栈就会自动完成后续封包和路由。
从工程文件结构来看,这套代码不是单个.c文件,而是完整的 IAR 工程,包含ZMain、Components、Projects、Source等目录。其中Source目录下有几个关键文件:SensorApp.c、SensorApp.h、OSAL_Sensor.c和ADC_Sensor.c。OSAL_Sensor.c是协议栈操作系统的任务注册入口,在osalInitTasks()里添加了光敏传感器任务初始化函数;SensorApp.c里则处理周期采样事件和无线数据发送。下面我先把目录结构列出来,然后进入核心代码讲解。
2.3 代码目录结构:从裸机驱动到协议栈应用层
拿到这份代码后,首先看到的不是一堆散落的.c文件,而是一个完整可导入的 IAR for 8051 工程目录。顶层目录大致如下:
CC2530_LightSensor_Project/ ├── Components/ │ ├── hal/ │ │ ├── target/ │ │ │ └── CC2530EB/ │ │ │ ├── hal_adc.c │ │ │ └── hal_adc.h │ │ ├── include/ │ │ └── common/ │ ├── stack/ │ │ ├── af/ │ │ ├── nwk/ │ │ ├── sapi/ │ │ └── zcl/ │ └── osal/ │ ├── common/ │ └── include/ ├── Projects/ │ ├── zstack/ │ │ └── Samples/ │ │ └── LightSensor/ │ │ ├── CC2530DB/ │ │ │ ├── LightSensor.eww │ │ │ ├── LightSensor.ewp │ │ │ └── f8wConfig.cfg │ │ └── Source/ │ │ ├── SensorApp.c │ │ ├── SensorApp.h │ │ ├── OSAL_Sensor.c │ │ └── ADC_Sensor.c └── Tools/ └── 烧录脚本/这里的hal_adc.c是 TI 底层库提供的 ADC 驱动,但这份代码里的ADC_Sensor.c没有直接调用它,而是选择直接操作寄存器,这样更容易控制采样时序,也方便把光敏传感器的采样逻辑独立出来。Projects/zstack/Samples/LightSensor/CC2530DB下是真正的工程文件,用 IAR 打开.eww就能加载整个 workspace。
工程中定义了一个周期性事件SENSOR_SAMPLE_EVT,当 OSAL 系统启动后,应用层事件处理函数会周期性进入该事件分支,触发一次 ADC 采样,然后经过滤波和数据处理,最后调用AF_DataRequest()上报。这个设计把「硬件采样」和「无线协议」完全解耦:ADC_Sensor.c只负责拿到数值,SensorApp.c只负责把数值变成 ZigBee 报文。后面你要换传感器,只需要替换ADC_Sensor.c内部的采集函数即可,应用层代码几乎不用动。
3. 核心模块拆解:光敏采集、滤波与上报链路
3.1 光敏电阻电压读取:ADC 采样配置与寄存器操作
在ADC_Sensor.c里,光敏采样的入口函数是Sensor_ReadLightADC(),它负责完成所有寄存器配置并返回原始 ADC 结果。CC2530 的 ADC 有两条路径:一条是单次转换,一条是连续转换。这里用的是单次转换,好处是功耗低、逻辑简单,适合传感器周期性唤醒的场景。代码直接抄了底层的启动流程,但把通道和参考电压的配置抽成了可改参数,方便换引脚。
#include "ioCC2530.h" #define LIGHT_SENSOR_ADC_CH 6 // P0_6 对应 AIN6 #define ADC_REF_AVDD5 0x00 // 参考电压选择 AVDD5 #define ADC_12_BIT 0x08 // 12位分辨率 unsigned int Sensor_ReadLightADC(void) { unsigned int adcValue = 0; // 1. 配置 ADC 通道、分辨率、参考电压 ADCCON2 = (ADC_12_BIT | ADC_REF_AVDD5 | LIGHT_SENSOR_ADC_CH); // 2. 触发单次转换 ADCCON1 |= 0x40; // 3. 等待转换完成 while (!(ADCCON1 & 0x80)); // 4. 读取 12 位结果(高 8 位在 ADCL,低 8 位在 ADCH) adcValue = (ADCL >> 2) | ((unsigned int)ADCH << 6); return adcValue; }逻辑说明:第 1 步把ADCCON2按位组合,ADC_12_BIT这个值 0x08 实际上对应的是ADCCON2中SHDIV字段里表示 12 位模式的位置,以及启动转换时需要的额外配置。第 2 步通过置ADCCON1的 bit6 来启动一次转换。第 3 步循环等待ADCCON1的 bit7 变为 1,表示采样完成。第 4 步是 CC2530 特有的结果拼接方式:ADCL里低 2 位是无效位,所以右移两位丢弃,ADCH是结果的高 8 位,左移 6 位拼到adcValue的 bit6 到 bit13,最终得到 0~4095 的 12 位无符号数。
参数说明:LIGHT_SENSOR_ADC_CH决定从哪个引脚采集,如果你把光敏模块接到了 P0_0,就把这个值改成 0。ADC_REF_AVDD5选择参考电压为 3.3V(AVDD5 实际就是芯片工作电压)。对 12 位分辨率来说,adcValue / 4096 * 3.3V就是光敏电阻分压点的电压值。这段代码没有做多次采样,因为单次采样可能带上电源噪声,所以我们在下一节用软件滤波来处理。
3.2 数字滤波:均值滤波与阈值映射
直接读回来的 ADC 值通常不稳定,尤其是用电池供电或者附近有电机、继电器干扰时,数值会在 ±50 范围内乱跳。这套代码在SensorApp.c里实现了一个简单的滑动均值滤波:连续采样 5 次,去掉最大值和最小值,然后对剩余 3 次求平均。这个思路比简单平均抗尖峰干扰,又比卡尔曼滤波简单得多,适合 MCU 资源有限的场景。
#define SAMPLE_COUNT 5 unsigned int Sensor_FilterLightADC(void) { unsigned int raw[SAMPLE_COUNT]; unsigned int sum = 0; unsigned int min, max; unsigned char i; // 连续采集 SAMPLE_COUNT 次 for (i = 0; i < SAMPLE_COUNT; i++) { raw[i] = Sensor_ReadLightADC(); // 增加一点延时,让 ADC 内部电容充分放电 asm("NOP"); } // 找最大值和最小值 min = raw[0]; max = raw[0]; for (i = 1; i < SAMPLE_COUNT; i++) { if (raw[i] < min) min = raw[i]; if (raw[i] > max) max = raw[i]; } // 跳过最大最小值求和 sum = 0; for (i = 0; i < SAMPLE_COUNT; i++) { if (raw[i] != min && raw[i] != max) { sum += raw[i]; } } // 返回平均(中间值个数是 3) return sum / (SAMPLE_COUNT - 2); }逻辑说明:第一次循环里连读 5 次 ADC,每次读完后插入一个 NOP 空指令,目的是给 ADC 采样保持电容一点时间,避免连续读时通道切换带来的串扰。第二次循环找出最大和最小值,第三个循环求和时跳过这两个极端值,最后除以 3,得到的结果就比单次采样的稳定很多。这个滤波方法在光照缓慢变化的场景下不会丢失实时性,因为光照本身就不是快速变化的量,5 次采样消耗的总时间不到 1ms,对 ZigBee 上报周期来说影响可以忽略。
拿到滤波后的 ADC 值,下一步是映射成光照等级。常见做法是把 ADC 值按电压线性映射成 0~100 的百分比光照强度,因为光敏电阻本身就是非线性元件,精确计算 Lux 需要标定曲线。代码里给了一个实用的映射函数:
unsigned char Sensor_CalcLightLevel(unsigned int adcValue) { unsigned int volt_mv; unsigned char level; // 参考电压 3.3V,12 位分辨率,换算成 mV volt_mv = (unsigned long)adcValue * 3300 / 4096; // 分压关系:VLux = VDD * R2 / (R1 + R2),根据实测范围定阈值 if (volt_mv < 500) level = 0; // 黑暗 else if (volt_mv < 1200) level = 1; // 弱光 else if (volt_mv < 2200) level = 2; // 一般室内 else level = 3; // 明亮 return level; }这里我把光照分成了 4 档,阈值对应的电压值是我在某次做模拟项目X时按 10kΩ 分压电阻实测来的:室内关灯时电压约 300mV,开台灯时约 800mV,正常日光灯下约 1800mV,靠窗白天能到 2800mV 以上。你移植到不同环境后,可以用串口把volt_mv打印出来,根据自己的场景重新划分档位。如果你要更细腻的连续值,就不要用等级,直接把volt_mv或 adcValue 放到数据帧里上报即可。
3.3 无线上报:基于 ZigBee 协议栈的端点与簇设计
光敏数据处理好之后,要交给协议栈发送。SensorApp.c里用了一个自定义事件SENSOR_REPORT_EVT来周期触发上报。之所以不放在hal层直接调串口,是想让这个节点能真正成为一个独立的 ZigBee 设备。下面这段是上报的核心函数:
static void Sensor_SendReport(void) { afAddrType_t dstAddr; uint8 lightData[2]; // 组装上报数据:高字节为光照等级,低字节为 ADC 原始值高 8 位 lightData[0] = Sensor_GetLightLevel(); lightData[1] = (uint8)(Sensor_FilterLightADC() >> 4); // 目标地址:协调器端点的单播地址 dstAddr.addrMode = (afAddrMode_t)Addr16Bit; dstAddr.addr.shortAddr = 0x0000; // 协调器的短地址 dstAddr.endpoint = 0x08; // 协调器上对应的端点 // 调用协议栈发送接口 AF_DataRequest(&dstAddr, &SensorTaskDesc, SENSOR_LIGHT_CLUSTERID, 2, lightData, NULL, &Sensor_AckCallback, AF_DISCV_ROUTE); }逻辑说明:dstAddr是协议栈定义的地址结构体,Addr16Bit表示使用 16 位短地址寻址,0x0000是协调器固定短地址。endpoint设为 8,两边都要一致。SENSOR_LIGHT_CLUSTERID是我在头文件里定义的 0xFF01,用于区分不同数据类型。AF_DataRequest的参数依次是:目标地址、任务描述符、簇 ID、数据长度、数据指针、确认序号(NULL 表示不申请重传)、回调函数、路由发现选项。发送完成后,结果会通过Sensor_AckCallback回调返回,你可以在这个回调里检查状态是不是afStatus_SUCCESS。
关于数据帧格式,这个设计把光照等级和 ADC 高位分开,一个字节存等级方便协调器直接做逻辑判断,另一个字节存 ADC 值方便上位机复现原始电压。如果你要发送完整 12 位 ADC 值,通常要拆成两个字节,并在帧头放一个标识。这版代码为了简练只发了高 8 位,但保留了扩展思路。
所有上报逻辑都由 OSAL 的事件驱动。在OSAL_Sensor.c的任务初始化里,启动了一个定时器:
void Sensor_TaskInit(void) { // 注册应用层事件处理函数 SensorTaskId = osal_set_event(Sensor_TaskID, SENSOR_REPORT_EVT); }然后在事件回调里,每 10 秒触发一次上报。周期修改也很简单,在SensorApp.c的Sensor_ProcessEvent函数里重新调用osal_start_timerEx即可。后续改造我会专门讲怎么改成 1 秒或 30 秒。
4. 环境搭建与运行:把代码烧进 CC2530 的实际步骤
4.1 IAR 工程导入与编译器配置
这套代码是标准的 IAR for 8051 工程,不是 Keil 或 GCC 工程。你需要安装 IAR Embedded Workbench for 8051,版本建议 8.10 以上,对 CC2530 支持比较好。拿到源码后,先用 IAR 打开Projects/zstack/Samples/LightSensor/CC2530DB/LightSensor.eww。如果打开后编译报错,最常见原因是工程里的芯片型号没选对,需要在 Project -> Options -> General Options -> Target 里选择Chip: CC2530F256。
接着检查编译器优化等级。Z-Stack 官方例程里默认优化等级是High - Speed,但光敏传感器代码里涉及寄存器操作,如果开启高优化,可能把ADCCON1的判断语句优化掉,导致代码停在 while 循环里。我一般把这个选项改成Low或Medium,稳定性优先。具体位置:Options -> C/C++ Compiler -> Optimizations -> Level选Low。
还要注意一个坑:f8wConfig.cfg文件里配置了协调器、路由器、终端设备的编译选项。如果你要烧录的是终端节点,确保宏定义ZDO_COORDINATOR没有被启用,否则节点会试图建网。工程里已经把这部分配置好了,但你自己新建工程时经常会遇到「节点一直无法加入网络」的问题,原因就在这里。
4.2 烧录工具与仿真器连接
烧录 CC2530 有三种常见方式:通过 TI 的 SmartRF04EB 仿真器、通过 CC Debugger,或者通过串口 Bootloader。这套代码默认走仿真器下载,因为编译完成后可以直接用 IAR 的 Debug 按钮下载。接线很简单:仿真器的 10 针 JTAG 接口对接到开发板的 P0_2(DD)、P0_3(DC)和复位脚。注意不要接反,有的板子丝印是 DC 在上,有的是 DD 在上,我第一次接反时直接把仿真器保护熔丝烧了,血泪经验。
如果你的板子上没有 JTAG 接口,只有串口,那就需要先烧一版 Bootloader。但大多数 CC2530 模块都会预留 JTAG,所以直接用仿真器最稳妥。烧录时 IAR 会提示选择仿真器型号,选TI CC2530后,如果下载失败,先检查仿真器驱动是否安装,通常用TI SmartRF Flash Programmer测试一下能不能识别芯片。
4.3 串口调试与协调器端数据验证
这套代码的终端节点本身没有串口输出,因为我们默认它通过 ZigBee 无线上报。要验证数据,你需要一个协调器节点,负责接收并打印数据。最简单的验证方法:用一个 CC2530 协调器开发板,烧录 TI 官方例程GenericApp或者任何能打印接收数据的代码,把串口接到电脑,波特率设为 115200,然后上电组网。
给光敏传感器节点供电后,它会自动扫描信道并加入协调器所在的网络。加入成功后,协调器端的串口会看到类似Light Level: 2, Raw: 0x045A的打印信息。如果你手头没有协调器代码,也可以暂时把光敏节点的AF_DataRequest注释掉,改成调用二维码串口打印函数,比如HAL_UART_WriteString("Light Level:..."),先验证采集部分,再验证无线部分。这样分步调试,出问题时更容易定位。
下面我给出一个常见的手动触发采样调试方法:在SensorApp.c的按键事件里,每次按 SW1 键强制调用一次Sensor_SendReport(),这样即使没等到上报周期,也能立刻看到数据。具体在按键回调里加上:
if (keys & HAL_KEY_SW1) { Sensor_SendReport(); }这个简单改动在调试时非常实用,相当于给无线上报增加了一个手动触发开关,不用傻等那 10 秒。
5. 避坑与常见问题排查:我在 CC2530 光敏项目里踩过的坑
5.1 读到的 ADC 值一直是 4095 或 0
现象:光敏传感器在正常光照下,读回来的原始 ADC 值不是 4095 就是 0,完全没有中间值。不管怎么改变光照,结果都不变。
原因:这个问题几乎都是引脚配置错误导致的。CC2530 的 P0 引脚既可以做 GPIO,也可以做模拟输入,默认状态下是 GPIO 功能。如果不把引脚寄存器P0SEL对应位置为 1,ADC 根本采集不到外部信号,读出来的结果就可能是电源电压或者 0。另外一个常见原因是分压电路没共地,或者光敏电阻两端接反,导致电压直接落在 VDD 或 GND 上。
解决:在Sensor_ReadLightADC()开头加上引脚模拟功能配置:
P0SEL |= (1 << LIGHT_SENSOR_ADC_CH); // 配置为外设功能(ADC输入)同时用万用表测一下你接的那个抽头电压,确认电压在 0.1V~3.0V 之间随光照变化。如果电压不变,检查光敏电阻是不是焊错位置了。
5.2 光照强度突变但上报数据没变化
现象:拿手电筒直接照光敏电阻,串口打印的光照等级仍然停留在 2 或 3,要过好几秒才反应过来,甚至一直不变。
原因:这套代码里的均值滤波函数Sensor_FilterLightADC()没有考虑瞬态响应。如果你在 10 秒上报周期里只调用一次滤波,每次滤波内部连续采 5 次,但光照可能在两次采样间已经跳变。另外,如果SENSOR_SAMPLE_EVT事件里没有及时重新启动定时器,上报周期会变得越来越长,看起来就是数据不更新。
解决:在事件处理函数里,每次上报结束后必须重新osal_start_timerEx启动下一次事件。正确的写法是:
if (events & SENSOR_REPORT_EVT) { Sensor_SendReport(); // 重新启动10秒定时器 osal_start_timerEx(Sensor_TaskID, SENSOR_REPORT_EVT, 10000); return (events ^ SENSOR_REPORT_EVT); }如果你需要更快响应,可以把Sensor_FilterLightADC()的连续采样次数减少到 3 次,并把上报周期改成 2 秒。光照传感器本身不需要 1ms 级响应,2 秒足够感知大部分环境变化。
5.3 ZigBee 组网后数据收不到
现象:节点和协调器指示灯都表明已经入网,但协调器串口就是打印不出光敏数据。节点上的AF_DataRequest返回状态一直是afStatus_NO_ROUTE或者afStatus_FAILED。
原因:大多数情况下是端点号不匹配。协调器如果只注册了端点 1,而光敏节点发往端点 8,数据会被协议栈丢弃。其次可能是簇 ID 不一致,协调器端没有注册SENSOR_LIGHT_CLUSTERID这个簇,应用层不会把数据交给用户处理。再有就是源地址模式搞错了,有的协调器程序用的组播地址,而光敏节点发的是单播短地址。
解决:先抓包看节点是否真的发出了数据帧。如果你没有 USB 抓包器,就在协调器的接收回调里打印所有收到的未知端点数据,临时改成广播发送试试:
// 临时将目标地址改为广播 dstAddr.addrMode = (afAddrMode_t)AddrBroadcast; dstAddr.addr.shortAddr = 0xFFFF;这样做能确认协议栈本身通不通。如果广播能收到数据,说明问题在单播路由或端点配置,再回头核对两边的端点号和簇 ID。之前我做某跨平台系统时,就是因为协调器工程里默认端点号是 1,而我自己定的 8,导致整整查了两天。
5.4 电池供电后采集电压漂移严重
现象:用 USB 供电时读数稳定,换成两节 AA 电池供电,光照等级频繁跳变,ADC 值上下浮动接近 200。
原因:电池供电时电源电压会随着负载变化而跌落,尤其当 ZigBee 发射瞬间电流高达 40mA,电池电压瞬间被拉低。而代码里 ADC 参考电压选的AVDD5,即参考电压跟随电源电压变化,所以光合值就随电源波动漂移。这是 CC2530 这类内置 ADC 的常见痛点。
解决:三种方案。一是改用内部 1.25V 参考电压,这样 ADC 结果不会随 VDD 漂移,但你的分压电路输出必须不超过 1.25V,否则会溢出,需要重新分压到适合范围。二是软件校准,在每次上报前读一次电源电压,用 ADC 值除以参考电压归一化。三是先射后看,把模块放在可调电源下,从 3.0V 到 3.6V 扫一遍,找到偏移规律,然后在代码里做电压补偿。这套代码默认用 AVDD5,如果是做电池供电节点,我强烈建议你把ADC_REF_AVDD5改成内部参考电压0x10,并把分压电阻调大。当你看到光照等级稳定了,这个坑才算真正迈过去。
6. 二次开发思路:把单点光敏节点改造成可用的小系统
6.1 修改采样周期与数据包结构
若要让这套代码适应你自己的项目,第一步就是改上报周期。在SensorApp.c中找到osal_start_timerEx调用,把 10000 改成你需要的值即可,单位是毫秒。注意这个定时器是单次触发的,所以必须在事件处理中重新启动,否则只上报一次。如果你要同时上报光照、温度和湿度,可以扩展lightData数组长度,并把不同传感器数据依次填入,同时协调器端也要调整解析逻辑。
6.2 增加阈值报警和控制输出
光敏传感器最常见的业务场景不是单纯上报,而是天黑自动开灯、大白天拉窗帘。可以在Sensor_CalcLightLevel返回等级后,加一个阈值判断:
#define LIGHT_THRESHOLD_DARK 0 if (Sensor_GetLightLevel() == LIGHT_THRESHOLD_DARK) { // 触发继电器输出,或者发送开灯控制命令 Sensor_ControlLight(ON); } else { Sensor_ControlLight(OFF); }Sensor_ControlLight可以是直接控制 P1_0 引脚的 GPIO 函数,也可以是向另一个 ZigBee 开关节点发送控制帧。如果是后者,只需把目标地址从协调器改成开关节点的短地址,簇 ID 换成一个控制簇即可。这样就把一个采样节点升级成了控制节点。
6.3 低功耗优化:休眠与定时唤醒
CC2530 的终端设备支持 PM2 休眠模式,休眠电流微安级,非常适合电池供电。你要做的改造是:在Sensor_SendReport()完成后,调用osal_pwrmgr_task_state(Sensor_TaskID, PWRMGR_CONSERVE),让协议栈进入低功耗模式,同时用睡眠定时器替代软件定时器。等休眠时间到了,芯片自动唤醒。这个改造有几个注意点:必须把POWER_SAVING宏打开,且不能用带 GPSK 的 SensorApp 方式,否则整个任务都会阻塞。我把它写成了一个简单的流程表格:
| 步骤 | 改动位置 | 关键代码/配置 |
|---|---|---|
| 使能低功耗 | f8wConfig.cfg | POWER_SAVING宏设为 TRUE |
| 任务管理 | SensorApp.c | 上报完成后调用PWRMGR_CONSERVE |
| 唤醒源 | 使用睡眠定时器 | osal_start_timerEx会自动处理 |
| ADC 采样 | 唤醒后重新初始化 | 因为休眠期间 GPIO 和 ADC 配置会丢失 |
确实,休眠会带来一个麻烦:CC2530 从 PM2 唤醒后,GPIO 配置不会保留,必须重新设置引脚模式,否则 ADC 会失灵。所以我在Sensor_FilterLightADC()里每次采集前都会重新调用P0SEL |= (1 << LIGHT_SENSOR_ADC_CH);这一步,宁可多花一条指令,也不想起床后一直读到 0。
从那以后,每次改造这套代码,我都强制走一遍「确认引脚模式、确认定时器重启、确认端点匹配」这三步。就算改的是别人的工程,也一定要先烧一版原版代码,验证数据通路正常,再动手换传感器。这个习惯帮我避开过无数个隐藏的坑。希望这篇文章的细节能帮到你,让这套 CC2530 光敏传感器代码少走几段弯路。
本文还有配套的精品资源,点击获取