目录
背景
声明
开发环境
正文
一、接收中断 + 空闲中断
二、接收中断 + T35定时器中断
T35定时器
三、空闲中断 + DMA + 循环队列
背景
在单片机开发过程中,串口通讯是一种非常常用的串行通通讯方式,如调试、协议通信、模组驱动等都有大量的应用,而针对串口接收从技术角度可分定长数据接收及不定长数据接收。针对定长数据接收,可使用特定的起始和结束符以及长度进行帧识别,或其它有效的方式;而针对不定长数据接收为本文讨论的重点,起始不定长数据接收已包含了定长数据,因此掌握不定长数据接收是串口编程的重中之重,本文将使用3种方式由浅入深进行讨论,希望能带来一些收获。
声明
本文叙述的4中方式及代码并非适用于所有内核,仅供思路参考,实际应用中需自行验证可行性。
开发环境
主控 | STM32F103VET6 |
IDE | KEIL |
| |
正文
在单片机接收不定长数据时,重点是如何判断一帧数据接收完成,并能够将这一帧数据的长度和数据缓存下来,那么我们要的功能就实现了。下面通过网络分享的资源及工作时的经验,总结出以下几种思路实现供不同实际应用场合应用。
一、接收中断 + 空闲中断
STM32单片机串口实现了空闲帧检测功能,配合接收缓冲区非空格中断,当一帧数据接收时,利用接收缓冲区非空中断将一个一个字节的数据存入我们自定义的缓存区,并维护一个变量记录帧长度。当空闲中断发生时,意味着一帧的数据已经结束,通过标志置位即可判断这一帧已经结束。
优点:在自动识别帧数据,无需主程序参与
缺点:1. 针对其它厂家或其它型号单片机,不一定支持空闲中断功能
2. 在串口速率较高时,会导致频繁进入串口中断(接收中断)
部分程序:
/* 变量定义 */
#define DATA_RECV_MAXSIZE (128)
typedef struct data_recv_st
{
uint8_t buff[DATA_RECV_MAXSIZE] //uint8_t* buff;
uint16_t len;
uint8_t flag;
}data_recv_t;
data_recv_t data_recv;
uint8_t at_rxbuffer[1];
/* 串口初始化 */
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
HAL_UART_Receive_IT(&huart1,aRxBuffer1,1); // 开启接收中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 开启空闲中断
}
/* 中断函数处理 */
/**
* @brief This function handles USART1 global interrupt / USART1 wake-up interrupt through EXTI line 25.
*/
void USART1_IRQHandler(void)
{
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_ORE) != RESET ) {
__HAL_UART_CLEAR_OREFLAG(&huart1);
}
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_NE) != RESET ) {
__HAL_UART_CLEAR_NEFLAG(&huart1);
}
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_FE) != RESET) {
__HAL_UART_CLEAR_FEFLAG(&huart1);
}
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_PE) != RESET) {
__HAL_UART_CLEAR_PEFLAG(&huart1);
}
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)
{
if(data_recv.len < DATA_RECV_MAXSIZE) {
data_recv.buff[data_recv.len++] = USART1->RDR;
} else {
USART1->RDR;
}
}
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
data_recv.flag = 1;
}
}
二、接收中断 + T35定时器中断
我们已经实现了接收中断 + 空闲中断的方式,但是这种方式有一个缺点,有些单片机不支持空闲中断。先回到第一种方式,STM32单片机使用空闲中断检测帧是如何实现的呢?通过判断一个字节时间内是否有接收到数据。那么我们可以人为实现,通过一个基本定时器,当接收到新字节时刷新定时器,如果没有接收到新字节,那么定时器在设定的时间后会出现更新中断,在进入这个中断后我们就可以认为一帧数据已经结束了。那么我们如何实现这个定时器呢?在modbus协议中采用了这种方式,并且有个通用的叫法:T35定时器。
T35定时器
参考链接:https://www.cnblogs.com/mrsandstorm/p/5701867.html
波特率:每秒钟通过信道传输的信息量称为位传输速率,也就是每秒钟传送的二进制位数,简称比特率。比特率表示有效数据的传输速率,用b/s 、bit/s、比特/秒,读作:比特每秒。如9600b/s:指总线上每秒可以传输9600个bit;
若按照 1字节起始位 + 8字节数据位 + 2字节停止位 = 11位,那么在9600bps波特率下,可传输 9600 / 11 = 872.7273 个字节,那么一个1字节传输耗费的时间为 1 / 872.7273 = 0.0011s ,按照3.5个桢长度为超时时间计算,超时时间: 0.0011 * 3.5 = 0.0039 s,那么对于50us定时器来说,需要设置的定时周期为 0.0039s / 50us = 7.8个周期
通用计算公式:
3.5 * (1 / ( Baudrate / 11) * 100000) / 50
-> 3.5 * (1100000 / Baudrate) / 50
-> 3.5 * 220000 / Baudrate
-> 7 * 220000 / (Baudrate * 2)
T35定时器实现
基本定时器使用50us的时基,按照不同的波特率计算出需要n个50us,那么定时器的超时时间为:n*50 us
static void eMBHwInit(u32 ulBaudRate)
{
u32 usTimerT35_50us;
/* If baudrate > 19200 then we should use the fixed timer values
* t35 = 1750us. Otherwise t35 must be 3.5 times the character time.
*/
if( ulBaudRate > 19200 )
{
usTimerT35_50us = 100; /* 5000us. */
}
else
{
/* The timer reload value for a character is given by:
*
* ChTimeValue = Ticks_per_1s / ( Baudrate / 11 )
* = 11 * Ticks_per_1s / Baudrate
* = 220000 / Baudrate
* The reload for t3.5 is 1.5 times this value and similary
* for t3.5.
*/
usTimerT35_50us = ( 7UL * 220000UL ) / ( 2UL * ulBaudRate );
}
xMBPortTimersInit((u16)usTimerT35_50us);
}
优点:在自动识别帧数据,无需主程序参与
缺点:1. 需另外增加一个软件定时器实现T35定时器。
2. 在串口速率较高时,会导致频繁进入串口中断(接收中断)
程序:
该方式与第一种实现类似,第一种在空闲中断中处理的内容需转至定时器超时中断中进行并在串口中使能接收中断。
static void xMBPortTimersInit(u16 usTim1Timerout50us)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
u16 PrescalerValue = 0;
TIM_RCC;
//定时器时间基配置说明
//HCLK为72MHz,APB1经过2分频为36MHz
//TIM4的时钟倍频后为72MHz(硬件自动倍频,达到最大)
//TIM4的分频系数为3599,时间基频率为72 / (1 + Prescaler) = 20KHz,基准为50us
//TIM最大计数值为usTim1Timerout50u
PrescalerValue = (u16) (SystemCoreClock / 20000) - 1;
//定时器1初始化
TIM_TimeBaseStructure.TIM_Period = (uint16_t) usTim1Timerout50us;
TIM_TimeBaseStructure.TIM_Prescaler = PrescalerValue;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM, &TIM_TimeBaseStructure);
//预装载使能
TIM_ARRPreloadConfig(TIM, ENABLE);
TIM_ClearITPendingBit(TIM,TIM_IT_Update);
TIM_ITConfig(TIM, TIM_IT_Update, DISABLE);
TIM_Cmd(TIM, DISABLE);
}
static void eMBHwStart(void)
{
TIM_ClearITPendingBit(TIM, TIM_IT_Update);
TIM_ITConfig(TIM, TIM_IT_Update, DISABLE);
TIM_SetCounter(TIM,0x0000);
TIM_Cmd(TIM, DISABLE);
}
void uart_T35Init(u32 ulBaudRate)
{
eMBHwInit(ulBaudRate);
eMBHwStart();
}
三、空闲中断 + DMA + 循环队列
在前面两种中,都有共同的缺点:在速率较高,数据量较大时会频繁进入串口中断,如果在中断中不及时处理会导致ORE错误。那么有没有办法能够解决这个问题呢?答案肯定是有的。目前的问题是接收中断导致的问题,如果不使用接收中断,是不是可以解决问题。在STM32中可以使用DMA解决,DMA实现从外设到内存的传输,这样MCU可以在主程序中处理其它任务,而且不会进入接收中断。但是有个问题,DMA传输大数据量时如果只有一个缓存区,那么必然存在数据的拷贝,因为这个缓存区需要接收新的数据,旧的帧数据必须拷贝到其它数据中保存好。这就会引入另外一个问题:https://blog.csdn.net/qq_20999867/article/details/92961110。
那么我考虑的是使用一个队列,DMA每次接收完一个缓存区后去获取下一个缓存区的地址循环接收,并且维护一个头尾节点保证设备读取正常。
优点:效率较高,支持高速率传输
缺点:实现复杂
Lib_queue.c
#include "lib_queue.h"
#include <string.h>
/**
* @description: 创建队列
* @return {*} 0: 成功 -1: 失败
* @param {data_queue_t} *queue: 队列对象
* @param {void} *buf: 队列缓存数组指针
* @param {unsigned short} queue_len: 队列长度
* @param {unsigned short} itemsize: 队列单个元素大小,以字节为单位
*/
int data_queue_init(data_queue_t *queue, data_frame_complete_callback_t callback)
{
assert_param_queue(queue, return queue_false);
memset(queue, 0, sizeof(data_queue_t));
queue->front = queue->rear = 0;
queue->pop_state = QUEUE_STATE_UNLOCKED;
queue->push_state = QUEUE_STATE_UNLOCKED;
queue->len = DATA_QUEUE_LEN;
queue->complete_callback = callback;
return queue_true;
}
/**
* @description: 获取队列已存放个数
* @return {*}
* @param {data_queue_t} *queue
*/
int data_queue_use_block(data_queue_t *queue)
{
assert_param_queue(queue, return queue_false);
if (queue->front < queue->rear)
{
return (queue->front + queue->len - queue->rear);
}
else
{
return (queue->front - queue->rear);
}
}
/**
* @description: 获取剩余队列数据个数
* @return {*}
* @param {data_queue_t} *queue
*/
int data_queue_remain_block(data_queue_t *queue)
{
assert_param_queue(queue, return queue_false);
if (queue->front < queue->rear)
{
return (queue->rear - queue->front) - 1;
}
else
{
return (queue->len - (queue->front - queue->rear)) - 1;
}
}
/**
* @description: 获取队列总数据个数
* @return {*}
* @param {data_queue_t} *queue
*/
int data_queue_total_block(data_queue_t *queue)
{
assert_param_queue(queue, return queue_false);
return (queue->len - 1);
}
/**
* @description: 判断数据是否已满
* @return 满: queue_true 不满:queue_false
* @param {data_queue_t} *queue
*/
int data_queue_is_full(data_queue_t *queue)
{
assert_param_queue(queue, return queue_false);
if (data_queue_use_block(queue) == data_queue_total_block(queue))
{
return queue_true;
}
return queue_false;
}
/**
* @description: 判断数据是否已空
* @return 空: queue_true 非空:queue_false
* @param {data_queue_t} *queue
*/
int data_queue_is_empty(data_queue_t *queue)
{
assert_param_queue(queue, return queue_false);
if (data_queue_use_block(queue) == 0)
{
return queue_true;
}
return queue_false;
}
/**
* @description: 获取队列空闲块存储地址,获取存储地址后配合DMA或其他方式使用
* @return {*}
* @param {data_queue_t} *queue
*/
data_packet_t *data_queue_get_idleblock_addr(data_queue_t *queue)
{
assert_param_queue(queue, return NULL);
if (queue->push_state == QUEUE_STATE_LOCKED)
{
return NULL;
}
if (data_queue_is_full(queue) == queue_true)
{
return NULL;
}
queue->push_state = QUEUE_STATE_LOCKED;
return (&queue->data_packet[queue->front]);
}
/**
* @description: 数据接收完成,推入队列,实际数据已存在队列中,但需处理头计数
* @return {*}
* @param {data_queue_t} *queue
*/
int data_queue_push(data_queue_t *queue)
{
assert_param_queue(queue, return queue_false);
if (queue->push_state != QUEUE_STATE_LOCKED)
{
return queue_false;
}
queue->front++;
queue->front = queue->front % queue->len;
if (queue->complete_callback != NULL)
{
queue->complete_callback();
}
queue->push_state = QUEUE_STATE_UNLOCKED;
return queue_true;
}
/**
* @description: 获取队列数据,并处理尾计数
* @return {*}
* @param {data_queue_t} *queue
*/
data_packet_t *data_queue_pop(data_queue_t *queue)
{
data_packet_t *p_data_pack = NULL;
assert_param_queue(queue, return NULL);
if (data_queue_is_empty(queue) == queue_true)
{
return NULL;
}
if (queue->pop_state == QUEUE_STATE_LOCKED)
{
return NULL;
}
queue->pop_state = QUEUE_STATE_LOCKED;
p_data_pack = &queue->data_packet[queue->rear];
queue->rear++;
queue->rear = queue->rear % queue->len;
queue->pop_state = QUEUE_STATE_UNLOCKED;
return p_data_pack;
}
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed_queue(unsigned char *file, unsigned int line)
{
#ifdef PRINTE
printf("Wrong parameters value: file %s on line %d\r\n", file, line);
#endif
}
Lib_queue.h
#ifndef __LIB_QUEUE_H
#define __LIB_QUEUE_H
#define queue_true (0)
#define queue_false (-1)
#ifndef NULL
#define NULL ((void*)0)
#endif
void assert_failed_queue(unsigned char *file, unsigned int line);
#ifndef assert_param_queue
#define assert_param_queue(expr, action) {if(expr == 0) { assert_failed_queue((unsigned char *)__FILE__, __LINE__);action; }}
#endif
#define DATA_PACKET_SIZE (128)
#define DATA_QUEUE_LEN (10)
typedef void (*data_frame_complete_callback_t)(void);
typedef enum data_queue_lockstate_et
{
QUEUE_STATE_UNLOCKED,
QUEUE_STATE_LOCKED,
}data_queue_lockstate_t;
typedef struct data_packet_st
{
unsigned short len;
unsigned char data[DATA_PACKET_SIZE + 4];
}data_packet_t;
typedef struct data_queue_st
{
data_packet_t data_packet[DATA_QUEUE_LEN];
unsigned short front; // 数据头,指向下一个空闲存放地址
unsigned short rear; // 数据尾,指向第一个数据
unsigned short len;
data_queue_lockstate_t push_state;
data_queue_lockstate_t pop_state;
data_frame_complete_callback_t complete_callback;
}data_queue_t;
int data_queue_init(data_queue_t *queue, data_frame_complete_callback_t callback);
int data_queue_push(data_queue_t *queue);
data_packet_t *data_queue_get_idleblock_addr(data_queue_t *queue);
data_packet_t * data_queue_pop(data_queue_t *queue);
int data_queue_use_block(data_queue_t *queue);
int data_queue_remain_block(data_queue_t *queue);
int data_queue_total_block(data_queue_t *queue);
#endif
串口初始化
data_queue_t data_queue_recive;
data_packet_t *g_cur_data_packet = NULL;
void MX_USART1_UART_Init(void)
{
/* USER CODE BEGIN USART1_Init 0 */
/* USER CODE END USART1_Init 0 */
/* USER CODE BEGIN USART1_Init 1 */
/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 2000000;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART1_Init 2 */
extern void frame_complete_callback(void);
data_queue_init(&data_queue_recive, frame_complete_callback);
g_cur_data_packet = data_queue_get_idleblock_addr(&data_queue_recive);
HAL_UART_Receive_DMA(&huart1, g_cur_data_packet->data, DATA_PACKET_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //open idle interrupt
/* USER CODE END USART1_Init 2 */
}
中断函数处理
/**
* @brief This function handles USART1 global interrupt.
*/
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
HAL_UART_DMAStop(&huart1);
g_cur_data_packet->len = DATA_PACKET_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
DMA1_Channel5->CNDTR = DATA_PACKET_SIZE; //set DMA recive byte
data_queue_push(&data_queue_recive);
g_cur_data_packet = data_queue_get_idleblock_addr(&data_queue_recive);
if (g_cur_data_packet == NULL)
{
data_queue_pop(&data_queue_recive);
g_cur_data_packet = data_queue_get_idleblock_addr(&data_queue_recive);
}
HAL_UART_Receive_DMA(&huart1, g_cur_data_packet->data, DATA_PACKET_SIZE); //enable DMA
}
/* USER CODE END USART1_IRQn 1 */
}
版权声明:本文为CSDN博主「Hi,Mr.Wang」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_34672688/article/details/115473035
分享不易,点个赞再走吧☺☺☺