STM32
直播中

马占云

7年用户 1582经验值
私信 关注
[问答]

怎样去使用stm32f429的串口驱动模块呢

串口驱动具有哪些特点?
怎样去使用STM32f429的串口驱动模块呢?



回帖(2)

刘倩

2021-12-9 09:52:18
写在开头:这段时间在整理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;         
}


举报

蔡艳

2021-12-9 09:59:57
以上为环形队列的实现,我们可以将压入数据进缓存发在串口中断接收中,每接收到一个数据,便存入缓存,在应用函数中调用缓存数据弹出函数,一个字节一个直接的取出数据。
       在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);
}
举报

更多回帖

发帖
×
20
完善资料,
赚取积分