写在开头:这段时间在整理modbus协议时,发现没有一个比较方便使用的串口模块,因此结合之前的一些理解,将串口驱动整理出来。此串口驱动有以下特点:
发送接收均使用
DMA
- 串口配置不需要从刷固件便能修改,方便二次开发
- 数据接收有环形队列缓存,能接收不定长数据帧
- 使用读缓存函数能获取当前缓存帧数以及每帧的数据长度.
说明:
发送
:数据在发送过程中,首先被压入缓存,发送计时器会严格控制每条数据的发送时间间隔,发送使用DMA,以减轻CPU的负荷。
接收
:数据接收能实现不定长度接收,首先每帧数据都会通过DMA转移到接收缓存中,在每一帧数据到来时,会产生DMA空闲中断,此时会将数据帧的长度存入帧长度缓存。通过帧长度缓存,能够建立接收缓存中每一帧数据的索引,以及能得出缓存中剩余数据帧的个数。因此,在从缓存取出数据时,操作方便,避免程序主循环运行周期导致数据帧取出时周期不固定(这一点后面详细讨论)。
二 .软件实现
硬件平台为stm32f429,移植到其他平台只需要将初始化改此对应平台即可。
1.初始化
串口初始化一般分为以下步骤:1.时钟初始化;2.端口初始化; 3.DMA初始化;4.中断初始化。因此建立一个串口类型结构体SERIAL_INIT_TYPE。在使用串口时,定义相应串口号结构体变量,初始化时赋初值完成串口初始化。多数人习惯使用宏定义,对串口的每个引脚,波特率等使用宏定义,然后串口的打开使用宏开关,实现最大程度的消除重复代码。以定义变量的方式来操作同样是为了消除重复代码,但更为重要的一点是,参数灵活性得到提高。既然是变量,值是受控的,因此设备在运行过程中可以通过软件更改端口参数。在很多产品中,都会附带一个上位机软件,以来改变设备的配置(伺服驱动器等),驱动器本身的固件是不变的,属于“一次开发”,而上位机软件属于二次开发,目的是为了适当调整参数,令设备达到最优运行状态。宏定义虽然能减少代码量,但这种预编译处理产生的软件功能限制较死,每次增添功能需要重新刷固件。因此,二次开发也是本文强调的重点。
以下是结构体成员:
以下是结构体成员:
以下是结构体成员:
typedef struct
{
struct
{
/*有关时钟的配置项*/
}rcc_cfg;
struct
{
/*有关串口的配置项*/
}port_cfg;
struct
{
/*有关DMA的配置*/
}dma_cfg;
struct
{
/*有关中断的配置*/
}nvic_cfg;
}SERIAL_INIT_TYPE;
下面分别介绍四个内嵌结构体成员,分别是时钟结构体,串口结构体,dma结构体,中断结构体。
a.时钟结构体
struct /*rcc */
{
uint32_t rxPORT; /*Port_RCC*/
uint32_t txPORT;
uint32_t USART; /*USART_RCC*/
uint32_t rxDMA; /*DMA_RCC*/
uint32_t txDMA;
}rcc_cfg;
串口相关的时钟一般为引脚时钟,串口时钟,dma时钟,这个需要查找硬件手册以确定其值。
b.串口结构体
struct /*port*/
{
uint32_t baud; /*波特率*/
USART_TypeDef* USARTx; /*串口号*/
GPIO_TypeDef* rxPORT; /*串口接收引脚端口号*/
GPIO_TypeDef* txPORT; /*串口发送引脚端口号*/
uint16_t rxPIN; /*串口接收引脚引脚号*/
uint16_t txPIN; /*串口发送引脚引脚号*/
uint8_t rxAF; /*接收引脚复用*/
uint8_t txAF; /*发送引脚复用*/
uint8_t rxSOURCE; /*接收源*/
uint8_t txSOURCE; /*发送源*/
}port_cfg;
c.dma结构体
struct /*dma*/
{
uint32_t rxCHANNEL; /*接收通道*/
uint32_t txCHANNEL; /*发送通道*/
uint32_t txFLAG; /*发送完成标志*/
DMA_Stream_TypeDef* rxSTREAM ; /*接收dma数据流*/
DMA_Stream_TypeDef* txSTREAM ; /*发送dma数据流*/
USART_TypeDef* USARTx; /*串口号*/
uint8_t rxbuff[FIFO_SIZE]; /*接收缓存*/
uint8_t txbuff[FIFO_SIZE]; /*发送缓存*/
uint8_t fifo_record[FRAME_SIZE]; /*接收帧长度缓存*/
uint16_t record_point; /*帧长度缓存指针*/
uint16_t length; /*缓存长度*/
uint16_t tail; /*缓存尾指针*/
uint16_t head; /*缓存头指针*/
}dma_cfg;
d.中断结构体
struct /*nvic*/
{
uint8_t usart_channel; /*串口中断通道*/
uint8_t usart_Preemption; /*抢占优先级*/
uint8_t usart_Sub; /*从优先级*/
uint8_t dma_txchannel; /*dma中断通道*/
uint8_t dma_txPreemption; /*抢占优先级*/
uint8_t dma_txSub; /*从优先级*/
}nvic_cfg;
以下是初始化串口1示例:
SERIAL_INIT_TYPE usart1=
{ .rcc_cfg.USART = RCC_APB2Periph_USART1, /*时钟*/
. rcc_cfg.rxPORT = RCC_AHB1Periph_GPIOA,
.rcc_cfg.txPORT = RCC_AHB1Periph_GPIOA,
.rcc_cfg.rxDMA = RCC_AHB1Periph_DMA2,
.rcc_cfg.txDMA = RCC_AHB1Periph_DMA2,
.port_cfg.USARTx = USART1, /*串口*/
.port_cfg.baud = 115200,
.port_cfg.rxPORT = GPIOA,
.port_cfg.txPORT = GPIOA,
.port_cfg.rxPIN = GPIO_Pin_10,
.port_cfg.txPIN = GPIO_Pin_9,
.port_cfg.rxAF = GPIO_AF_USART1,
.port_cfg.txAF = GPIO_AF_USART1,
.port_cfg.rxSOURCE = GPIO_PinSource10,
.port_cfg.txSOURCE = GPIO_PinSource9,
.dma_cfg.USARTx = USART1, /*dma*/
.dma_cfg.rxCHANNEL = DMA_Channel_4,
.dma_cfg.txCHANNEL = DMA_Channel_4,
.dma_cfg.txSTREAM = DMA2_Stream7,
.dma_cfg.rxSTREAM = DMA2_Stream5,
.dma_cfg.txFLAG = DMA_FLAG_TCIF4,
.dma_cfg.head = 0,
.dma_cfg.tail = 0,
.dma_cfg.length = FIFO_SIZE,
.nvic_cfg.usart_channel = USART1_IRQn, /*中断*/
.nvic_cfg.usart_Preemption = 0,
.nvic_cfg.usart_Sub = 1,
.nvic_cfg.dma_txchannel = DMA2_Stream7_IRQn,
.nvic_cfg.dma_txPreemption = 0,
.nvic_cfg.dma_txSub = 2
};
定义以以上串口描述结构体,之后构建初始化函数,初始化时,只需要将以上变量传入初始化函数,即可完成不同串口的初始化。
初始化函数和串口类型结构体相对应,4个块分别初始化。
void usart_config(SERIAL_INIT_TYPE* usart)
{
usart_rcc_cfg(usart);
usart_nvic_cfg(usart);
usart_dma_cfg(usart);
usart_port_cfg(usart);
}
具体的初始化过程详细见源码。
2.数据发送
初始化完成后,dma发送控制已经设置为指定数据地址(txbuff【】)内容到串口,此时只需要要将数据放进发送缓存中,启动dma发送,数据就能发送出去。
注意:在使用DMA发送时,遇到一个问题,数据只能发送一帧。产生的原因为:dma发送完成后,发送完成标志位被置一,即使在不使能发送中断的情况下,完成标志位也会影响下一帧数据的发送,因此这里使能发送中断,发送完成将标志位清零。
void DMA1_Stream6_IRQHandler(void) /*dma发送中断*/
{
if(DMA_GetFlagStatus(DMA1_Stream6,DMA_FLAG_TCIF6)!=RESET)
{
DMA_ClearFlag(DMA1_Stream6,DMA_FLAG_TCIF6);
}
}
static void start_dma(SERIAL_INIT_TYPE* usart,u16 ndtr) //启动dma
{
/*使能DMA*/
DMA_Cmd(usart->dma_cfg.txSTREAM, DISABLE);
while(DMA_GetFlagStatus(usart->dma_cfg.txSTREAM,usart->dma_cfg.txFLAG) != DISABLE){}
DMA_SetCurrDataCounter(usart->dma_cfg.txSTREAM,ndtr);
/* USART1 向 DMA发出TX请求 */
/*使能DMA*/
DMA_Cmd(usart->dma_cfg.txSTREAM, ENABLE);
}
由于这里对发送时序没有严格要求,因此一启动dma数据立即会发送,但对时序有严格要求的系统,比如某些传感器会有最高频率限制要求,这时就需要加入发送计时器,以一定周期来发送,适应外部低速设备。
3.数据接收
串口接收数据我们知道会经常用到两种中断:字节中断和帧中断。一个是每接收到一个字节的数据便产生一次中断,另一个是接收到一帧数据后产生中断。使用这两种方式配合,便能实现,串口简单的接收处理,只不过接收是实时的。在接收数据处理不及时的情况下,容易出现丢包情况。
环形缓存:
在通讯设备中经常听到缓存一词,缓存是避免丢包的有效措施。关于环形缓存的概念,这里不提及,百度有详细的介绍。以下是环形队列的实现方法:
a .创建队列结构体
#define FIFO_MAX_LEN 200
typedef struct
{
uint16_t head; //队列头
uint16_t tail; //队列尾
uint16_t len; //当前队列数据长度
uint8_t buff[FIFO_MAX_LEN]; //队列数组
}u8FIFO_TYPE;
b.队列初始化
void fifo_init(pu8FIFO_TYPE fifo)
{
fifo->head=0;
fifo->tail=0;
fifo->len=0;
}
c.队列判断空满
bool is_fifo_empty(pu8FIFO_TYPE fifo)
{
if(fifo->head==fifo->tail && !fifo->len) /*头尾相等,长度为0为空*/
return 1;
else
return 0;
}
/*判满*/
bool is_fifo_full(pu8FIFO_TYPE fifo)
{
if(fifo->len>=FIFO_MAX_LEN) /*长度大于等于最大容量为满*/
return 1;
else
return 0;
}
d.存数据与取数据
/*数据压入缓存*/
bool push_to_fifo(pu8FIFO_TYPE fifo , uint8_t data)
{
if( ! is_fifo_full(fifo))
{
fifo->buff[fifo->tail]=data;
fifo->len++;
/*存入一个字节,尾指针加一当到达最大长度,将尾指针指向0,
以此达到头尾相连的环形缓存区*/
fifo->tail=(fifo->tail+1) % FIFO_MAX_LEN;
return 1;
}
else
return 0;
}
/*缓存数据弹出*/
bool pop_from_fifo(pu8FIFO_TYPE fifo,uint8_t* data)
{
*data = fifo->buff[fifo->head]; /*取出一个字节数据*/
/*头指针加1,到达最大值清零,构建环形队列*/
fifo->head=(fifo->head+1) % FIFO_MAX_LEN;
if(!fifo->len)
{
fifo->len--;
return 1;
}
else
return 0;
}