一、整体功能概述
这套程序模拟了一个真实的无线传感器网络(如抄表系统)的通信流程:
-
主机(Master / 采集端): 负责发起通信。它首先发送“暗号”(
kunkun)寻找从机,收到从机的确认(zhiyin)后,采集自身的 ADC 电压数据,将其转换并打包发送给从机。最后,它会等待从机将收到的数据原样发回,以验证通信链路的绝对可靠性。 - 从机(Slave / 接收网关): 一直处于监听状态。收到“暗号”后立刻回复确认。随后接收主机发来的 ADC 数据,并触发 LED 闪烁,最后将该数据原样“回声(Echo)”给主机。
int32_t main(void)
{
// 1. 硬件初始化
System_Init_Config();
// 2. 射频初始化
if (rf_init() != OK)
{
while(1); // 失败报警
}
rf_set_default_para();
// 3. 初始状态设置 (编译时决定)
#ifdef SLAVE_MODE
// [从机] 上电必须开启接收,否则听不到第一句。接收超时窗口(Timeout)
/*
情况 A(提前下班): 如果第 2 秒钟,主机发来了 kunkun(快递到了),
单片机会立刻抓起数据去处理,这个 15 秒的倒计时会瞬间作废,根本不需要等满 15 秒。
情况 B(超时放弃): 如果苦苦等了整整 15 秒,主机都没发信号(可能主机没开机),
从机就不会再傻等下去了。它会触发一个“接收超时(RXTIMEOUT)”的标志位,然后重新安排下一次的监听。
*/
rf_enter_single_timeout_rx(15000);
/*
因为在无线通信中,从机是“被动方”。它刚上电的时候,
完全不知道主机什么时候会开口说话(主机可能 2 秒发一次,也可能 10 分钟发一次)。
所以,从机一开机,必须立刻把“耳朵”张开,并且给一个足够长的时间(15 秒),
确保在这个宽裕的时间段内,至少能“逮住”主机的一次呼叫。一旦逮住一次,双方就建立起联络了。
*/
#endif
// [主机] 不需要预先接收,它会主动发送
while (1)
{
// === 1. 优先处理中断 (公共逻辑) ===
if (g_bIrqTriggered)
{
g_bIrqTriggered = 0;
rf_irq_process(); // SPI 读取状态
}
// === 2. 业务逻辑 (编译时二选一) ===
#ifdef MASTER_MODE
OnMaster();
#endif
#ifdef SLAVE_MODE
OnSlave();
#endif
}
}
#ifndef __FUN_H
#define __FUN_H
// ==========================================
// 模式配置开关
// ==========================================
// 方案 A:如果是主机,保留这行,注释掉 SLAVE_MODE
#define MASTER_MODE
// 方案 B:如果是从机,保留这行,注释掉 MASTER_MODE
//#define SLAVE_MODE
// --- 安全检查 (防止你忘了选,或者两个都选了) ---
#if defined(MASTER_MODE) && defined(SLAVE_MODE)
#error "错误:不能同时定义 MASTER_MODE 和 SLAVE_MODE!请注释掉一个。"
#elif !defined(MASTER_MODE) && !defined(SLAVE_MODE)
#error "错误:你没有定义任何模式!请在 fun.h 中选择 MASTER_MODE 或 SLAVE_MODE。"
#endif
#include "main.h"
// 函数声明
// 这样写的好处是:无论什么模式,main.c 都能看到这两个函数声明
// 避免编译报错,虽然我们在 main 里只会调用其中一个
void OnSlave(void);
void LedToggle(void);
void OnMaster(void);
void Pulse_PA8(void);
uint16_t Get_ADC_Value(void);
uint16_t Get_ADC_Average(uint8_t times);
#endif
#include "fun.h"
#include "init.h"
#include "delay.h"
#include "radio.h"
#include "buffer.h"
// ============================================
// 1. 数据定义 (硬编码 kunkun 和 zhiyin)
// ============================================
// 为了防止 buffer.c/h 未定义,我们在局部定义一份
static uint8_t Kunkun[] = {'k', 'u', 'n', 'k', 'u', 'n'};
static uint8_t Zhiyin[] = {'z', 'h', 'i', 'y', 'i', 'n'};
#define DATA_LEN 6
// --- 补回丢失的全局变量 ---
double Rssi_dBm; // 用于存储最近一次接收信号的强度 (单位: dBm)
double Snr_value; // 用于存储最近一次接收信号的信噪比 (单位: dB)
// 外部变量声明
extern struct RxDoneMsg RxDoneParams;
extern uint8_t rx_test_buf[];
// ============================================
// 2. 公共函数
// ============================================
void LedToggle(void)
{
GPIO_WritePin(LED_PORT, LED_PIN, GPIO_Pin_RESET); // 亮
// Delay_Ms(200);
GPIO_WritePin(LED_PORT, LED_PIN, GPIO_Pin_SET); // 灭
// Delay_Ms(200);
}
// ============================================
// 3. 主机模式代码 (MASTER_MODE)
// ============================================
#ifdef MASTER_MODE
static uint32_t tx_time = 0;
extern volatile uint8_t g_bIrqTriggered; // 引用 main.c 定义的变量
void OnMaster(void)
{
// ==========================================
// 阶段一:握手 (Handshake)
// 发送 "kunkun" 确认从机在线
// ==========================================
// 1. 发送 "kunkun"
if (rf_single_tx_data(Kunkun, DATA_LEN, &tx_time) != OK)
{
return;
}
// 2. 等待发送完成 (带防死锁的中断处理)
while (rf_get_transmit_flag() == RADIO_FLAG_IDLE)
{
if (g_bIrqTriggered)
{
g_bIrqTriggered = 0;
rf_irq_process();
}
}
rf_set_transmit_flag(RADIO_FLAG_IDLE); // 清除发送标志
// 3. 进入接收模式,等待从机回复 "zhiyin"
rf_enter_single_timeout_rx(200);
// 4. 等待接收完成 (带防死锁)
while (rf_get_recv_flag() == RADIO_FLAG_IDLE)
{
if (g_bIrqTriggered)
{
g_bIrqTriggered = 0;
rf_irq_process();
}
}
// ==========================================
// 阶段二:判断握手结果 & 发送 ADC 数据
// ==========================================
// 检查是否收到了数据 (RXDONE)
if (rf_get_recv_flag() == RADIO_FLAG_RXDONE)
{
rf_set_recv_flag(RADIO_FLAG_IDLE); // 清除接收标志
// 5. 验证内容是否为 "zhiyin"
if (RxDoneParams.Size == DATA_LEN &&
RxDoneParams.Payload[0] == 'z' &&
RxDoneParams.Payload[1] == 'h')
{
// --- 握手成功!开始处理数据转换 ---
// A. 读取原始 ADC 码值 (0~4095)
uint16_t adc_raw = Get_ADC_Average(8);
// B. 【核心修改】转换为十进制电压值 (mV)
// 公式:(adc_raw * 3300) / 4096
// 使用 (uint32_t) 强制转换防止乘法溢出
uint16_t voltage_mv = (uint16_t)((uint32_t)adc_raw * 3300 / 4096);
// C. 打包数据 (将16位电压值拆分)
uint8_t tx_buffer[DATA_LEN];
tx_buffer[0] = (uint8_t)(voltage_mv >> 8); // 电压高8位
tx_buffer[1] = (uint8_t)(voltage_mv); // 电压低8位
// 填充剩余字节
for(int k=2; k< DATA_LEN; k++)
{
tx_buffer[k] = 0;
}
// D. 发送转换后的电压数据给从机
Delay_Ms(5);
g_bIrqTriggered = 0;
// 启动发送
rf_single_tx_data(tx_buffer, DATA_LEN, &tx_time);
// E. 等待发送完成
uint32_t timeout_safety = 0;
while (rf_get_transmit_flag() == RADIO_FLAG_IDLE && timeout_safety < 10000000)
{
if (g_bIrqTriggered)
{
g_bIrqTriggered = 0;
rf_irq_process();
}
timeout_safety++;
}
rf_set_transmit_flag(RADIO_FLAG_IDLE);
// ==========================================
// 阶段三:等待回声校验 (Echo Check)
// ==========================================
// F. 进入接收,等待从机把刚才的电压值发回来
rf_enter_single_timeout_rx(200);
// 等待接收完成
while (rf_get_recv_flag() == RADIO_FLAG_IDLE)
{
if (g_bIrqTriggered)
{
g_bIrqTriggered = 0;
rf_irq_process();
}
}
// 检查是否收到回传数据
if (rf_get_recv_flag() == RADIO_FLAG_RXDONE)
{
rf_set_recv_flag(RADIO_FLAG_IDLE);
// 1. 还原接收到的电压数据 (高8位< <8 | 低8位)
uint16_t echo_val = ((uint16_t)RxDoneParams.Payload[0] < < 8) | RxDoneParams.Payload[1];
// 2. 验证:发出的电压值 == 收回的电压值?
// 注意:这里必须对比转换后的 voltage_mv,逻辑才闭环
if (echo_val == voltage_mv)
{
// 通信链路完美闭环!
LedToggle();
}
}
}
}
else
{
// 握手失败 (接收超时或 CRC 错误)
rf_set_recv_flag(RADIO_FLAG_IDLE);
}
// ==========================================
// 阶段四:周期延时
// ==========================================
Delay_Ms(2000); // 2秒发送一次
}
// 占位函数
void OnSlave(void) {}
#endif // 结束 MASTER_MODE
// ============================================
// 4. 从机模式代码 (SLAVE_MODE)
// ============================================
#ifdef SLAVE_MODE
extern volatile uint8_t g_bIrqTriggered;
extern uint8_t *pData;
extern uint16_t slave_recv_val;
uint8_t slave_tx_buffer[DATA_LEN];
static uint32_t sl_tx_time = 0;
void OnSlave(void)
{
// --- 情况1:收到数据 (RXDONE) ---
if (rf_get_recv_flag() == RADIO_FLAG_RXDONE)
{
// 1. 读取参数 & 清除标志
Rssi_dBm = RxDoneParams.Rssi;
Snr_value = RxDoneParams.Snr;
rf_set_recv_flag(RADIO_FLAG_IDLE);
pData = RxDoneParams.Payload;
uint8_t len = RxDoneParams.Size;
// 2. 逻辑分支判断
if (len == DATA_LEN && pData[0] == 'k' && pData[1] == 'u' && pData[2] == 'n')
{
// === 分支 A:握手 (kunkun - > zhiyin) ===
LedToggle(); // 提示收到握手
Delay_Ms(5); // 避让延时,给主机切换接收留时间
g_bIrqTriggered = 0;
rf_single_tx_data(Zhiyin, DATA_LEN, &sl_tx_time);
}
else
{
// === 分支 B:数据回传 (Echo) ===
// [调试看这里]:此时 slave_recv_val 存储的是十进制电压(mV)
// 你在调试窗口看这个变量,取消 Hex 显示,就能看到 3200 左右的数字
slave_recv_val = ((uint16_t)pData[0] < < 8) | pData[1];
// 1. 准备发送缓冲区 (深拷贝)
for(int i = 0; i < DATA_LEN; i++)
{
slave_tx_buffer[i] = pData[i];
}
LedToggle(); // 提示收到数据
Delay_Ms(5); // 避让延时
g_bIrqTriggered = 0;
// 原样发回给主机进行校验
rf_single_tx_data(slave_tx_buffer, DATA_LEN, &sl_tx_time);
}
// ==========================================
// 3. 修正后的发送等待逻辑
// ==========================================
uint32_t timeout_safety = 0;
// 只要还没触发中断,就一直在这里等,同时处理可能的 SPI 任务
while (g_bIrqTriggered == 0 && timeout_safety < 1000000)
{
// 注意:PAN3031 的中断处理通常在主循环或此处调用
// 如果 g_bIrqTriggered 在 EXTI 中断里变1,循环会退出
timeout_safety++;
}
// 退出循环后,如果是正常中断触发,处理它
if (g_bIrqTriggered)
{
g_bIrqTriggered = 0;
rf_irq_process(); // 更新状态机,将 TX 状态转为 IDLE
}
rf_set_transmit_flag(RADIO_FLAG_IDLE);
// 4. 重新进入接收模式
// 建议保持 15 秒或更长,确保从机始终“醒着”等主机点名
rf_enter_single_timeout_rx(15000);
}
// --- 情况2:接收超时或错误 ---
if ((rf_get_recv_flag() == RADIO_FLAG_RXTIMEOUT) || (rf_get_recv_flag() == RADIO_FLAG_RXERR))
{
rf_set_recv_flag(RADIO_FLAG_IDLE);
rf_enter_single_timeout_rx(15000);
}
}
// 占位函数
void OnMaster(void) {}
#endif // 结束 SLAVE_MODE
// 获取一次 ADC 的原始值 (0~4095)
uint16_t Get_ADC_Value(void)
{
uint16_t value;
// 1. 启动转换
ADC_SoftwareStartConvCmd(ENABLE);
// 2. 等待转换完成 (注意:这里要用死循环等待标志位变高)
// 原来的代码 while(...) {...} 如果标志位没变高会直接跳过,是错的
while(ADC_GetITStatus(ADC_IT_EOC) == RESET);
// 3. 清除标志位
ADC_ClearITPendingBit(ADC_IT_EOC);
// 4. 读取数据
value = ADC_GetConversionValue();
return value;
}
// 获取滤波后的 ADC 值 (平均值滤波)
uint16_t Get_ADC_Average(uint8_t times)
{
uint32_t sum = 0;
uint8_t i;
for(i = 0; i < times; i++)
{
sum += Get_ADC_Value(); // 调用上面的基础函数
// 稍微延时一点点,防止采样过快读到同样的干扰
// 如果没有 Delay 函数,可以用空循环代替
}
return (uint16_t)(sum / times);
}
二、关键部分代码解析与注释
时间线:
- PAN3031 芯片收发完成,拉高物理引脚。
-
CW32 单片机被触发物理中断,进入
GPIOA_IRQHandler。 -
中断函数火速把
g_bIrqTriggered和pan3031_irq_trigged_flag这两个软件变量变成 1。 -
主程序
while循环看到g_bIrqTriggered == 1,立刻跳出等待。 -
主程序调用
rf_irq_process(),它看到pan3031_irq_trigged_flag == true,准许执行。 -
函数通过 SPI 查询 PAN3031 具体原因,最终将结果翻译成
RADIO_FLAG_RXDONE等业务标志位。
1.ADC 数据采集与处理(主机特有)
这是示例功能——获取的物理数据。
// ==========================================
// 阶段二:判断握手结果 & 发送 ADC 数据
// ==========================================
// 【关键功能 1:数据采集与转换】
// A. 读取原始 ADC 码值 (0~4095)
uint16_t adc_raw = Get_ADC_Average(8); // 多次采样求平均,起到软件滤波作用,抗干扰
// B. 【核心修改】转换为十进制电压值 (mV)
// 公式:(adc_raw * 3300) / 4096 (基于 3.3V 参考电压)
// 使用 (uint32_t) 强制转换防止 adc_raw * 3300 发生 16 位溢出
uint16_t voltage_mv = (uint16_t)((uint32_t)adc_raw * 3300 / 4096);
// C. 打包数据 (将16位电压值拆分成两个 8 位字节)
uint8_t tx_buffer[DATA_LEN];
tx_buffer[0] = (uint8_t)(voltage_mv >> 8); // 取电压高 8 位放入数组第 0 个位置
tx_buffer[1] = (uint8_t)(voltage_mv); // 取电压低 8 位放入数组第 1 个位置
2、回声校验机制 (Echo Check)
这是保证通信可靠性的重要手段。
// ==========================================
// 阶段三:等待回声校验 (Echo Check) (主机端)
// ==========================================
// F. 进入接收,等待从机把刚才的电压值原样发回来
rf_enter_single_timeout_rx(200);
// ... (等待接收代码略) ...
if (rf_get_recv_flag() == RADIO_FLAG_RXDONE)
{
rf_set_recv_flag(RADIO_FLAG_IDLE);
// 【关键功能 2:数据重组与校验】
// 1. 将收到的两个 8 位字节重新拼装成 16 位的电压数据
uint16_t echo_val = ((uint16_t)RxDoneParams.Payload[0] < < 8) | RxDoneParams.Payload[1];
// 2. 验证:发出的电压值 == 收回的电压值?
if (echo_val == voltage_mv)
{
// 通信链路完美闭环!说明数据在空中没有发生误码
LedToggle();
}
}
3、从机的多路分支处理
从机需要根据收到的数据内容,决定是进行“握手回应”还是“数据回传”。
// ============================================
// 4. 从机模式代码 (SLAVE_MODE)
// ============================================
// 2. 逻辑分支判断
// 【关键功能 3:协议解析】
// 检查收到的数据长度是否为 6,且前三个字符是否为 'k' 'u' 'n'
if (len == DATA_LEN && pData[0] == 'k' && pData[1] == 'u' && pData[2] == 'n')
{
// === 分支 A:握手协议 ===
LedToggle();
Delay_Ms(5); // 必须加!给主机从“发送态”切换到“接收态”留出时间,否则主机听不到回信
g_bIrqTriggered = 0;
rf_single_tx_data(Zhiyin, DATA_LEN, &sl_tx_time); // 回复确认暗号
}
else
{
// === 分支 B:数据回传 (Echo) ===
// 解析主机发来的 16 位电压数据,主要用于在调试窗口查看
slave_recv_val = ((uint16_t)pData[0] < < 8) | pData[1];
// 将收到的数据深拷贝到发送缓冲区
for(int i = 0; i < DATA_LEN; i++)
{
slave_tx_buffer[i] = pData[i];
}
LedToggle();
Delay_Ms(5);
g_bIrqTriggered = 0;
// 原样发回给主机进行最终校验
rf_single_tx_data(slave_tx_buffer, DATA_LEN, &sl_tx_time);
}
4、健壮的中断等待机制
摒弃了官方 SDK 的“死等”,加入了基于全局变量 g_bIrqTriggered 的等待逻辑。
// 【关键功能 4:防死锁等待机制】
uint32_t timeout_safety = 0;
// 只要没触发中断,且没有超时,就在这里等
while (g_bIrqTriggered == 0 && timeout_safety < 1000000)
{
timeout_safety++; // 软件超时计数器,防止硬件死机导致程序永久卡死
}
// 退出循环后,如果是正常中断触发,而不是超时退出的,则处理中断
if (g_bIrqTriggered)
{
g_bIrqTriggered = 0;
rf_irq_process(); // 底层处理函数,将状态标志位更新为 TXDONE 或 RXDONE
}
/**
* @brief This funcation handles GPIOA
*/
extern volatile uint8_t g_bIrqTriggered;
void GPIOA_IRQHandler(void)
{
// 检查是否是 PA1 引脚触发
if (CW_GPIOA->ISR_f.PIN1)
{
// 1. 必须先清除中断标志!
GPIOA_INTFLAG_CLR(bv1);
// 2. 【核心修改】通知主循环“有事发生了”!!!
// 只有加上这一句,OnMaster 里的 if (g_bIrqTriggered) 才会成立
g_bIrqTriggered = 1;
// 3. 调用原来的业务逻辑 (保留即可)
PAN3031_irq_handler();
}
}
审核编辑 黄宇
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。
举报投诉
-
通信程序
+关注
关注
0文章
15浏览量
8691 -
CW32
+关注
关注
1文章
323浏览量
1953
发布评论请先 登录
相关推荐
热点推荐
CW32量产烧录工具
线的程序烧录。 烧录器CW-Writer 一.烧录器概况 图1所示为烧录器的实物展示图。 图1 CW-Writer烧录器 当烧录器通过USB口和PC机连接实现供电和通信功能,当离线
【项目展示】基于CW32的遥控循迹小车
CW32循迹小车.zip_免费高速下载|百度网盘-分享无限制 一、概述 CW32循迹、遥控小车具有循迹和遥控两种功能,小车的硬件模块由CW32F030C8T6小蓝板、智能小车控制底板、BT04-E
【CW32无线抄表项目】W25Q+CW32程序示例
/Armink/SFUD 一、程序分析 硬件总线映射(引脚与时钟的“避坑点”) #define FLASH_SPIx CW_SPI2// 注意:CW32 中 SPI1 在 APB2 总线,而 SPI2 通常
基于芯源CW32 MCU的LED闪烁示例及代码分析
最近我在项目中使用了芯源的CW32 MCU,这是一款非常适合物联网和低功耗应用的微控制器。在初步学习和使用中,我做了一个简单的LED闪烁实验,通过这篇帖子给大家分享一下代码及相关的配置步骤。
硬件
发表于 12-04 06:52
CW32系列MCU在Eclipse GCC + JLink下的使用示例分享
CW32系列MCU在Eclipse GCC + JLink下的使用示例:
1、下载安装Eclipse IDE for Embedded C/C++ Developers。
2、下载安装
发表于 02-02 06:57
【应用笔记】CW32 自举程序中使用的 ISP 协议
CW32 自举程序中使用的 ISP 协议CW32 微控制器片上 FLASH 存储器有一部分区域用于存储 BootLoader 启动程序,在芯片出厂时已编程,用户可利用 BootLoad
发表于 06-06 13:26
cw32和stm32的区别
cw32和stm32的区别 CW32和STM32是两种常见的单片机,被广泛应用于各种电子设备中。在本文中,我们将深入探讨CW32和STM32之间的区别和优劣势。 1. 硬件性能 硬件性能是衡量单片机
应用笔记-CW32 自举程序中使用的 ISP 协议
CW32自举程序中使用的ISP协议CW32微控制器片上FLASH存储器有一部分区域用于存储BootLoader启动程序,在芯片出厂时已编程,用户可利用BootLoader启动
发表于 06-06 13:37
•7次下载
【CW32无线抄表项目】示例通信程序讲解
评论