目录
需要实现的功能
本系统旨在构建一个 PC → STM32(上位机)→ ZigBee → STM32(下位机)→ CAN 设备 的完整无线控制链路,具体功能如下:
-
下行控制路径(PC → 远端设备)
- 用户通过 PC 串口发送指令数据;
- 上位机端 STM32F103C8T6 通过 USART1 接收该数据,并利用 UART 空闲中断(IDLE)配合 DMA 实现不定长帧接收;
- 数据经由上位机的 USART2 通过 ZigBee DL-22 模块无线透传至下位机;
- 下位机端 STM32F103C8T6 通过 USART2 接收数据后:
- 将有效载荷封装为标准 CAN 帧(DLC ≤ 8),通过 CAN 总线发送给远端设备;
- 同时将原始数据回传至上位机端,用于状态确认或调试。
-
上行反馈路径(下位机端 → PC)
- 下位机端在完成 CAN 发送后,立即将接收到的数据原样通过 USART2 + ZigBee 回传;
- 上位机接收到该回传数据后,通过 USART1 转发给 PC;
- PC 串口助手可同时看到原始发送指令与下位机回传确认,形成闭环验证。
-
关键通信特性
- 支持不定长度数据帧传输(最大 8 字节/帧,适配 CAN DLC 限制);
- 使用 UART IDLE 中断 + DMA 实现高效、低 CPU 占用的串口收发;
- 上位机实现双通道并发 DMA 发送(一路转发至 ZigBee,一路回显至 PC);
- 下位机采用双缓冲 + 单帧排队机制,确保在 UART 发送繁忙时不会丢失新数据;
- 包含完善的错误检测与恢复机制(如清除 UART 错误标志、防止 Size=0 的误触发、过滤全零帧等);
- CAN 波特率时序经优化,采样点设置为 80%(BS1=7, BS2=2),符合工业推荐范围(75%~85%)。
最终效果:用户在 PC 串口调试助手中发送一帧十六进制数据(如
AB CD EF 12 34 56 78 90),可立即在同一窗口中看到两帧相同内容——一帧为本地回显,另一帧为下位机处理后的回传确认,表明整个“PC → ZigBee → CAN → 回传”链路工作正常。
需要的硬件及其IOC配置
硬件:2块C8T6,2块ZigBee DL22模块。
上位机端
C8T6在SystemCore里配置HSE为无源晶振(水晶/陶瓷晶振),Debug配置为Serial Wire。



Connectivity里配置USART1(与电脑通信)和USART2(与ZigBee模块通信),System Core 再配置DMA(或者从串口的界面可以直接跳转到配置DMA的界面),配置均默认。

System Core中DMA的配置:

DMA的好处是一次能搬运指定数量的数据,且减少CPU的负担。
NVIC配置,勾选USART1 global interrupt和USART2 global interrupt。

下位机端
C8T6System Core配置同上位机端,开USART1(连接ZigBee模块)和CAN(连接其他单片机)DMA、NVIC。

DMA作数据回显,配置如下:

CAN的配置和默认相比,改了Time Quanta in Bit Segment 1/2(简称BS1或BS2) 为7 Times/2Times。
这是由于需要让采样点(Sample Point)在75%~85%之间,这么设置后
采样点=(SYNC_SEG+BS1)/(总位时间)=(1+7)/(1+7+2)=80%。

中断的配置:

需要实现的软件设置
设想是电脑发数据,经过上位机端C8T6串口1,C8T6串口2发给下位机端C8T6串口2,下位机端C8T6发CAN信号给其他单片机,并通过串口2发信号上位机端给C8T6的串口2。
也一并设置了上位机端给下位机端发信,和上位机端接收下位机端发的信的时候,给电脑发数据。
上位机端
都在main.c中实现。关键点:
1. 使用HAL_UARTEx_ReceiveToIdle_DMA()容忍变长数据;
2. USART1 收到的数据保存在pc_rx_buf,随后用这块缓冲发两路DMA。
3. 用两个volatile标志同步两个TX的完成,再重启Start_PC_to_DN_RX()。
头文件
函数u1_printf_async()所需的头文件,u1_printf_async()这个函数留作备用,现工程中没有使用。
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
/* USER CODE END Includes */
宏定义
/* USER CODE BEGIN PM */
#define LEN 8
/* USER CODE END PM */
变量定义
/* USER CODE BEGIN PV */
// 接收缓冲
uint8_t u1_rxBuffer[8];
uint8_t u2_rxBuffer[8];
// PC-下位机 通道缓冲
static uint8_t pc_rx_buf[LEN];
// 下位机(down node)-PC 通道缓冲
static uint8_t dn_rx_buf[LEN];
volatile uint8_t pc2dn_tx_pending = 0; //USART1->USART2 正在DMA发送
volatile uint8_t pc_echo_tx_pending = 0; ////USART1->USART1 回显正在DMA发送
/* USER CODE END PV */
DMA流控制启动函数定义
/* USER CODE BEGIN PFP */
static inline void Start_PC_to_DN_Rx(void) {
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, pc_rx_buf, LEN);
// 禁用半传输中断,只关注接收完成或空闲
__HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT);
}
static inline void Start_DN_to_PC_Rx(void) {
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, dn_rx_buf, LEN);
// 禁用半传输中断,只关注接收完成或空闲
__HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
}
/* USER CODE END PFP */
串口相关函数定义
/* USER CODE BEGIN 4 */
/**
* @brief USART1 发送不定长度信息
*/
void u1_printf_async(char* fmt, ...) {
uint16_t len;
va_list ap;
va_start(ap, fmt);
uint8_t buf[200];
vsprintf((char*)buf, fmt, ap);
va_end(ap);
len = strlen((char*)buf);
// 使用中断发送,不阻塞
HAL_UART_Transmit_IT(&huart1, buf, len);
}
// 接收事件回调(转发+回显)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart == &huart1) {
// 启动两路DMA发送,USART1->USART2转发和USART1->USART1回显
pc2dn_tx_pending = 0;
pc_echo_tx_pending = 0;
if(HAL_UART_Transmit_DMA(&huart2, pc_rx_buf, Size) == HAL_OK)
pc2dn_tx_pending = 1;
if(HAL_UART_Transmit_DMA(&huart1, pc_rx_buf, Size) == HAL_OK)
pc_echo_tx_pending = 1;
// 如果两路都没成功启动,为避免卡死立即重启接收
if(pc2dn_tx_pending == 0 && pc_echo_tx_pending == 0)
Start_PC_to_DN_Rx();
// 注:如果成功启动了任意一路DMA,则在TxCpltCallback 会重启接收
}
else if (huart == &huart2) {
// DN -> PC
if (HAL_UART_Transmit_DMA(&huart1, dn_rx_buf, Size) != HAL_OK) {
// 如果发送失败,立即重启接收
Start_DN_to_PC_Rx();
// 成功则等huart1 Tx完成后在HAL_UART_TxCpltCallback() 中重启huart2 的接收
}
}
}
// 串口发送完成回调:在对应发送完成后,重启对应端的接收
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart2) {
if(pc2dn_tx_pending){
pc2dn_tx_pending = 0;
if(pc_echo_tx_pending == 0)
// 如果两路都完成,重启huart1 接收
Start_PC_to_DN_Rx();
}
else
Start_PC_to_DN_Rx();
}
else if (huart == &huart1) {
if(pc_echo_tx_pending){
pc_echo_tx_pending = 0;
if(pc2dn_tx_pending == 0)
Start_PC_to_DN_Rx();
}
// 刚完成的是 DN->PC 的发送,现在重启 huart2 接收
else
Start_DN_to_PC_Rx();
}
}
// 错误回调
static inline void Recover_UART(UART_HandleTypeDef *huart)
{
// 强制清除所有错误标志
__HAL_UART_CLEAR_FLAG(huart, UART_FLAG_PE);
__HAL_UART_CLEAR_FLAG(huart, UART_FLAG_FE);
__HAL_UART_CLEAR_FLAG(huart, UART_FLAG_NE);
__HAL_UART_CLEAR_FLAG(huart, UART_FLAG_ORE);
// 中止 DMA 接收/发送并重启
HAL_UART_AbortReceive(huart);
HAL_UART_AbortTransmit(huart);
}
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
Recover_UART(huart);
if (huart == &huart1) {
pc2dn_tx_pending = 0;
pc_echo_tx_pending = 0;
Start_PC_to_DN_Rx();
} else if (huart == &huart2) {
Start_DN_to_PC_Rx();
}
}
/* USER CODE END 4 */
下位机端
都在main.c中实现。关键点:
1. 串口接收空闲中断回调存在Size为0的可能(极短IDLE或误触发),程序中对Size进行判断,其为0则return,避免启动DMA发送0字节。
2. 使用双缓冲和单帧排队机制,当前TX正忙时,把新数据放入pending缓冲;等TxCpltCallback再发这帧。
3. 用“读SR再读DR”清除错误标志,避免宏在F1上不可靠。(参考链接:https://blog.csdn.net/lzhco/article/details/116095407)
4. CAN DLC限制用min(Size, 8)做了判断。
5. 丢弃全0数据,防止地不稳定的时候自激回环。
头文件
用于memcpy()的调用。
/* USER CODE BEGIN Includes */
#include <string.h>
/* USER CODE END Includes */
宏定义
/* USER CODE BEGIN PD */
#define LEN 8
/* USER CODE END PD */
变量定义
/* USER CODE BEGIN PV */
static uint8_t u1_buf[LEN]; // RX DMA写入缓冲(ReceiveToldle)
static uint8_t tx_buf[LEN]; //立即发送用的TX缓冲
static uint8_t tx_pending_buf[LEN]; //排队用的备用TX缓冲
static volatile uint8_t is_tx_busy = 0; //UART TX 正在DMA发送
static volatile uint8_t is_tx_pending = 0; //有一帧待发送
static volatile uint16_t pending_len = 0;
static CAN_TxHeaderTypeDef TxHeader;
static uint32_t TxMailbox;
/* USER CODE END PV */
串口相关函数定义
/* USER CODE BEGIN 4 */
static void Start_UART2_Rx(void)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, u1_buf, LEN);
// 禁用半传输中断
__HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
}
// 接收事件回调(下位机端)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart == &huart2) {
if(Size == 0){
Start_UART2_Rx();
return;
}
uint16_t n = (Size > LEN)? LEN:Size;
// 过滤错误帧
if (huart->ErrorCode != HAL_UART_ERROR_NONE){
Start_UART2_Rx();
return;
}
// 将接收到的数据复制到独立的TX缓冲,避免与下一次RX冲突
memcpy(tx_buf, u1_buf, n);
// 0. 丢弃全0帧,避免自激回环
uint8_t all_zero = 1;
for (uint16_t i = 0; i < n; ++i)
{
if (tx_buf[i] != 0x00){
all_zero = 0;
break;
}
}
if(all_zero){
Start_UART2_Rx();
return;
}
// 1. CAN发送(DLC<=8)
uint8_t dlc = (n > 8) ? 8 : n;
TxHeader.DLC = dlc;
(void)HAL_CAN_AddTxMessage(&hcan, &TxHeader, u1_buf, &TxMailbox);
// 2. 回传到上位机端
//如果UART2当前空闲,则直接启动DMA发送;否则排队一帧,等TxCplt再发
if (HAL_UART_GetState(&huart2) == HAL_UART_STATE_READY){
if(HAL_UART_Transmit_DMA(&huart2, tx_buf, n) == HAL_OK)
is_tx_busy = 1;
else{
// 发送启动失败,尝试排队一帧
memcpy(tx_pending_buf, tx_buf, n);
pending_len = n;
is_tx_pending = 1;
}
}
else{
// TX正忙,尝试排队一帧
memcpy(tx_pending_buf, tx_buf, n);
pending_len = n;
is_tx_pending = 1;
}
// 3.重启接收
Start_UART2_Rx();
}
}
// 发送完成回调:如有排队帧则继续发;否则标记空闲
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart != &huart2) return;
if(is_tx_pending){
// 取出排队帧发送
if(HAL_UART_Transmit_DMA(&huart2, tx_pending_buf, pending_len) == HAL_OK)
is_tx_pending = 0;
// 此时的tx_busy 仍为1,因为需要保持忙状态直到下一次发送完成
else{
// 启动失败,丢弃排队帧,标记空闲
is_tx_pending = 0;
is_tx_busy = 0;
}
}
else
is_tx_busy = 0;
}
// 错误回调(F1清错+重启RX)
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart != &huart2) return;
// 读SR->读DR清除错误标志
volatile uint32_t tmp;
tmp = huart->Instance->SR;
tmp = huart->Instance->DR;
(void)tmp;//给编译器说tmp 该丢弃
// 终止正在进行的收发,防止状态机卡死
HAL_UART_AbortReceive(huart);
HAL_UART_AbortTransmit(huart);
//清除状态,防止误判忙/在排队
is_tx_busy = 0;
is_tx_pending = 0;
pending_len = 0;
//立即重启接收
Start_UART2_Rx();
}
/* USER CODE END 4 */
实验结果
在 PC 端串口调试助手中以十六进制格式发送数据 ABCDEF1234567890,并设置为十六进制显示模式,可观察到两条内容相同的返回数据。这表明:上位机 STM32C8T6 成功接收了 PC 发送的指令,并将其转发至下位机;下位机处理后通过 ZigBee 将原始数据回传,上位机再将该回传数据转发回 PC。实验现象与系统设计功能完全一致。

浙公网安备 33010602011771号