完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
写在开头:这段时间在整理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; } |
|
|
|
以上为环形队列的实现,我们可以将压入数据进缓存发在串口中断接收中,每接收到一个数据,便存入缓存,在应用函数中调用缓存数据弹出函数,一个字节一个直接的取出数据。
在stm32 DMA中,接收DMA能够自动构建环形缓存区,类似上面fifo->tail=(fifo->tail+1) % FIFO_MAX_LEN; 实现方式。通过配置DMA初始化结构体成员:DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; 初始化完成后,用户只需要DMA_GetCurrDataCounter函数,便能获取当前缓存剩余容量,也就是队尾tail可以用以下公式获得:tail = MAX_LEN - DMA_GetCurrDataCounter();以上,实现了串口DMA接收数据, 由缓存区取单字节数据。 可以看出,每次只能从缓存取出一个字节的数据,也就是说每循环一次取一个数据。因此,取数据是和主循环周期相关的。如果我们能知道缓存中数据帧的长度即数量,那么我们就能一次取出一帧的数据。能实现这种,不定长数据接收自然也解决了。下面实现不定长度的接收。 4.不定长度数据接收 程序实现: a.串口空闲中断函数 void USART1_IRQHandler(void) { u16 data; static uint16_t count=0; static uint16_t last_tail=0; if(USART_GetITStatus(USART1,USART_IT_IDLE) != RESET) /*空闲中断*/ { data = USART1->SR; data = USART1->DR; /*清除中断标志*/ /*计算缓存尾指针*/ usart1.dma_cfg.tail=MAX_LEN - DMA_GetCurrDataCounter(usart1.dma_cfg.rREAM); /*尾指针由经过环形交接处*/ if(usart1.dma_cfg.tail< last_tail) usart1.dma_cfg.fifo_record[count] =FRAME_SIZE -last_tail + usart1.dma_cfg.tail; else usart1.dma_cfg.fifo_record[count]= usart1.dma_cfg.tail-last_tail; /*将计算的帧长度存入缓存*/ last_tail = usart1.dma_cfg.tail; /*构造帧长度环形缓存*/ count=(count+1) % FRAME_SIZE; } } b.从缓存中取数据 从缓存中取出一个字节: bool get_byte(SERIAL_INIT_TYPE* usart,uint8_t* data) { if(usart->dma_cfg.tail==usart->dma_cfg.head) //空或满 return 0; else //取数据 { *data = usart->dma_cfg.rxbuff[usart->dma_cfg.head]; usart->dma_cfg.head = (usart->dma_cfg.head+1)%500; return 1; } } 从缓存中取出一帧数据: bool get_str(SERIAL_INIT_TYPE* usart , uint8_t* frame, uint8_t* len) { uint8_t temp; temp=usart->dma_cfg.fifo_record[usart->dma_cfg.record_point]; if(temp) { *len = temp; usart->dma_cfg.fifo_record[usart->dma_cfg.record_point]=0; while(temp--) { if(get_byte(usart,frame++)){} else { usart->dma_cfg.record_point= (usart->dma_cfg.record_point+1)%usart->dma_cfg.length; return 0; } } usart->dma_cfg.record_point= (usart->dma_cfg.record_point+1)%FRAME_SIZE; return 1; } else return 0; } 取出字节函数为内部函数,面向上层的为取出一帧数据bool get_str(SERIAL_INIT_TYPE* usart , uint8_t* frame, uint8_t* len)当取出数据有效时,返回1.输入串口号。frame为取出数据帧后,暂时存储区的首地址。len指向存储数据帧长度的地址。即调用函数,数据帧以及长度会传到frame和len。 三 .接口实现 我们已经实现了串口的读写,初始化函数,为了能被上层更方便的调用。应该将接口统一化。定义以下机构体: typedef struct { void(*init)(SERIAL_INIT_TYPE* usart); void(*write)(SERIAL_INIT_TYPE* usart,uint8_t* data, uint16_t len); bool(*read)(SERIAL_INIT_TYPE* usart,uint8_t* data,uint8_t* len); }SERIAL_OPS_TYPE; 其中函数指针分别指向串口1的发送接收函数,初始化函数。当上层需要使用串口时。 按以下步骤实现(拿modbus_slave来举例): /*第一步定义modbus的发送接收端口类型,若存在其他接口类型比如can,同 样建立类似的驱动,定义一个输入can的类,然后在modbus中初始化时具体 选择哪种接口*/ SERIAL_OPS_TYPE md= { .init=usart_config, .write=usart_send, .read=get_str }; /*以下分别时modbus的初始化,发送,接收函数,淡化了串口号*/ void modbus_init(void) { md.init(&usart2); } /*发送指定长度数据*/ void modbus_write(uint8_t* frame,uint16_t len) { md.write(&usart2,frame,len); } /*读取一个字节*/ bool modbus_read(uint8_t* data,uint8_t* len) { return md.read(&usart2,data,len); } |
|
|
|
只有小组成员才能发言,加入小组>>
调试STM32H750的FMC总线读写PSRAM遇到的问题求解?
1115 浏览 1 评论
X-NUCLEO-IHM08M1板文档中输出电流为15Arms,15Arms是怎么得出来的呢?
1140 浏览 1 评论
572 浏览 2 评论
STM32F030F4 HSI时钟温度测试过不去是怎么回事?
428 浏览 2 评论
ST25R3916能否对ISO15693的标签芯片进行分区域写密码?
1026 浏览 2 评论
1614浏览 9评论
STM32仿真器是选择ST-LINK还是选择J-LINK?各有什么优势啊?
281浏览 4评论
STM32F0_TIM2输出pwm2后OLED变暗或者系统重启是怎么回事?
291浏览 3评论
276浏览 3评论
stm32cubemx生成mdk-arm v4项目文件无法打开是什么原因导致的?
253浏览 3评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-8-20 21:47 , Processed in 0.940832 second(s), Total 78, Slave 62 queries .
Powered by 电子发烧友网
© 2015 www.ws-dc.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号