扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

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

1小时前
165
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

整体功能概述

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

主机(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_MODEstatic 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_MODEextern 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 个位置

回声校验机制 (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();     }}

从机的多路分支处理

从机需要根据收到的数据内容,决定是进行“握手回应”还是“数据回传”。

 

// ============================================// 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);   }

健壮的中断等待机制

摒弃了官方 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();     }}

 


扫码加入QQ群3群| 610403240

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

以开放、共享、互助为理念,致力于构建武汉芯源半导体CW32系列MCU生态社区。无论是嵌入式MCU小自还是想要攻破技术难题的工程师,亦或是需求解决方案的产品经理都可在CW32生态社区汲取营养、共同成长。