【CW32无线抄表项目】示例通信程序讲解

描述

一、整体功能概述

这套程序模拟了一个真实的无线传感器网络(如抄表系统)的通信流程:

  1. 主机(Master / 采集端): 负责发起通信。它首先发送“暗号”(kunkun)寻找从机,收到从机的确认(zhiyin)后,采集自身的 ADC 电压数据,将其转换并打包发送给从机。最后,它会等待从机将收到的数据原样发回,以验证通信链路的绝对可靠性。
  2. 从机(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);
}

二、关键部分代码解析与注释

时间线:

  1. PAN3031 芯片收发完成,拉高物理引脚。
  2. CW32 单片机被触发物理中断,进入 GPIOA_IRQHandler
  3. 中断函数火速把 g_bIrqTriggeredpan3031_irq_trigged_flag 这两个软件变量变成 1。
  4. 主程序 while 循环看到 g_bIrqTriggered == 1,立刻跳出等待。
  5. 主程序调用 rf_irq_process(),它看到 pan3031_irq_trigged_flag == true,准许执行。
  6. 函数通过 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(); 
    }
}

审核编辑 黄宇
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分