STM32关于使用定时器来实现串口通信的整活实验

本文介绍了如何利用定时器和外部中断实现全双工串口通信,通过STM32F407VGTx单片机的TIM2,实现了波特率可配置的1位停止位、无校验的串口。作者详细讲解了从原理到代码实现的过程,包括接收和发送数据的优化,以及中断服务函数的编写。实验目标是创建一个与标准串口功能相当但无需硬件UART的虚拟串口,并提供了相应的接口函数。
摘要由CSDN通过智能技术生成

一、整活说明

在帮别人做项目的时候,写着代码唱着歌,突然就遇到硬件工程师把线连错了,串口模块连接的不是UART引脚,项目就这样暂停了吗?这能忍吗????这不能忍啊!作为一个资深(咸鱼)软件工程师务必不能被这种小事绊住手脚,项目还是要进行下去,所以就有了这次整活。

二、原理简介

串口原理就不重复了,直接网上随便一篇文章就能解决问题。在此说明使用定时器实现串口的理论依据,本篇所说的串口参数始终为8位数据位、1位停止位、无校验,并称传输一个bit所需的时间为一个tick,1 tick=(1000000/波特率)us。

先说接收

串口通信始终以一个下降沿开始维持一个tick的时间,然后开始以低位在前的形式发送数据,根据数据位来控制引脚电平,持续8 tick,最后是1 tick的高电平结束。由此可得出一个方案,中断设置为下降沿触发,然后打开定时器,每隔 1 tick 进入一次中断判断接收引脚的电平并存储,经过8 tick之后接收完毕,关闭定时器。

发送数据

由于串口通信的开始位和结束位都为 1 tick ,所以可以把这两个过程看作各是1bit数据。最终要发送的数据被组合为 tdr=(data<<1)|0x40,然后打开定时器,每隔 1 tick进入一次中断根据tdr最低位的值控制发送引脚的电平,经过10 tick之后发送完毕,关闭定时器。

优化

按照上面的思路,要实现一个双工串口岂不是要用2个定时器?这样做可以是可以,但是太大材小用了。作为一个爱兵如子的软件工程师,每个单片机外设都是极其重要的,如果一个外设不能发挥出它应有的光芒,想必作为一个外设,它也是不高兴的吧。实际实现用的是定时器的比较输出功能,理论上一个4通道定时器可以实现两路双工串口,只能说定时器YYSD,单片机不能没有定时器,就像西方不能没有耶路撒冷!
另外,由于串口通信需要频繁进中断并在中断中执行代码,为节省时间实际实现使用宏和寄存器的形式编程。

三、实验条件和目标

条件

本实验使用的硬件使用情况如下:

项目Value
单片机STM32F407VGTx
UART_TXPB10
UART_RXPB11
定时器TIM2
定时器通道1用于发送
定时器通道2用于接收
外部中断用于触发接收

你说PB10和PB11就是USART3的TX和RX脚啊,我有串口,但是我不用,哎嘿,就是玩~
都用串口了还叫整活吗?明显不叫啊,没这个理是不是?

目标

项目Value
参数1位停止位,无校验
波特率可配置
全双工
数据发送非阻塞,中断发送
数据接收缓冲区,中断接收

总之正常串口有的都要有,用户体验上不能有区别。

四、掉头发环节

新建文件 dev_vuart.h,dev_vuart.c

1.定义串口类

头文件 dev_vuart.h ,添加内容如下:

#ifndef dev_vuart_h__
#define dev_vuart_h__

#include "stdint.h"

typedef struct{
  int (*init)(void);
  int (*write)(const uint8_t *d,int len);
  int (*read)(uint8_t *d,int len);
  void *(*set_irqfun)(void (*fun)(uint8_t d,void *context),void *context);
}vuart_typedef;


vuart_typedef *vuart(void);

#endif

和普通串口接口相同,分为初始化,发送,接收,定义中断函数几个函数。

2.定义简单宏

串口需要频繁进入中断并在中断中处理数据,所以不宜使用库函数编程,定义简单宏如下,多数宏是根据stm32 hal库修改而来。

#define __MY_TIM  TIM2
#define TIM_CNT_MAX   0xffff

/*r{ 定义定时器运行频率,单位Mhz }c*/
#define TIM_FREQUENCY (84)
/*r{ 定义波特率 }c*/
#define VUART_RATE    (115200)


#define VUART_BIT_TICK  (TIM_FREQUENCY*(1000000/VUART_RATE))

#define __MY_TIM_ENABLE(__HANDLE__)                 ((__HANDLE__)->CR1|=(TIM_CR1_CEN))

#define __MY_TIM_DISABLE(__HANDLE__) \
  do { \
    if (((__HANDLE__)->CCER & TIM_CCER_CCxE_MASK) == 0UL) \
    { \
      if(((__HANDLE__)->CCER & TIM_CCER_CCxNE_MASK) == 0UL) \
      { \
        (__HANDLE__)->CR1 &= ~(TIM_CR1_CEN); \
      } \
    } \
  } while(0)

#define __MY_TIM_ENABLE_IT(__HANDLE__, __INTERRUPT__)    ((__HANDLE__)->DIER |= (__INTERRUPT__))

#define __MY_TIM_DISABLE_IT(__HANDLE__, __INTERRUPT__)   ((__HANDLE__)->DIER &= ~(__INTERRUPT__))
  
#define __MY_TIM_GET_FLAG(__HANDLE__, __FLAG__)          (((__HANDLE__)->SR &(__FLAG__)) == (__FLAG__))
  
#define __MY_TIM_CLEAR_FLAG(__HANDLE__, __FLAG__)        ((__HANDLE__)->SR = ~(__FLAG__))
  
#define __MY_TIM_CLEAR_IT(__HANDLE__, __INTERRUPT__)      ((__HANDLE__)->SR = ~(__INTERRUPT__))
  
#define __MY_TIM_GET_COUNTER(__HANDLE__)  ((__HANDLE__)->CNT)
  
#define __MY_TIM_GET_IT_SOURCE(__HANDLE__, __INTERRUPT__) (((__HANDLE__)->DIER & (__INTERRUPT__))== (__INTERRUPT__))
  
  
/*r{ 设置通道寄存器值,__CHANNEL__取值0~3 }c*/
#define __MY_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) \
    (*((&(__HANDLE__)->CCR1)+(__CHANNEL__)) = (__COMPARE__))

/*r{ 获取通道寄存器值,__CHANNEL__取值0~3 }c*/  
#define __MY_TIM_GET_COMPARE(__HANDLE__, __CHANNEL__) \
    (*((&(__HANDLE__)->CCR1)+(__CHANNEL__)))
 
#define __MY_SET_PIN(__GPIO__,__PIN__)  (__GPIO__->BSRR = __PIN__)
#define __MY_RESET_PIN(__GPIO__,__PIN__)  (__GPIO__->BSRR = (uint32_t)__PIN__ << 16U)

#define __MY_READ_PIN(__GPIO__,__PIN__) (__GPIO__->IDR & __PIN__)

/*r{ 打开外部中断;__INTERRUPT__取值0~22 }c*/
#define __MY_EXIT_ENABLE_IT(__INTERRUPT__)    (EXTI->IMR |= 1<<(__INTERRUPT__))

#define __MY_EXIT_DISABLE_IT(__INTERRUPT__)   (EXTI->IMR &= ~(1<<(__INTERRUPT__)))


3.定义私有数据

typedef struct
{
  int inited;// 是否初始化
  uint16_t tdr;// 模拟发送寄存器
  uint16_t rdr;// 模拟接收寄存器
  uint8_t tdr_left_bit;// 发送寄存器剩余bit数
  uint8_t rdr_left_bit;// 接收寄存器剩余bit数
  data_buff sbuff;// 发送缓冲区
  data_buff rbuff;// 接收缓冲区
  void (*irqfun)(uint8_t d,void *context);// 用户中断回调
  void *context;
  int in_send;// 正在发送
}self_data;

static self_data g_data;

以及一些操作宏:

// 开始接收
#define __VUART_RX_DR()\
      {g_data.rdr=0;\
        g_data.rdr_left_bit=8;\
        set_channel_after_tick(1,VUART_BIT_TICK*3/2);}

// 接收一个bit      
#define __VUART_RX_BIT()\
      {\
        if(g_data.rdr_left_bit>0)\
        {\
          if(__MY_READ_PIN(GPIOB,GPIO_PIN_11))\
          {\
            g_data.rdr|=1<<(8-g_data.rdr_left_bit);\
          }\
          g_data.rdr_left_bit--;\
        }\
        set_channel_after_tick(1,VUART_BIT_TICK);\
      }  

// 开始发送            
#define __VUART_SET_DR(d)\
      {g_data.tdr=((uint16_t)(d)<<1)|0x0200;\
      g_data.tdr_left_bit=1+8+1;\
      set_channel_after_tick(0,VUART_BIT_TICK);}

// 发送一个bit
#define __VUART_TX_BIT()\
      {\
        if(g_data.tdr_left_bit)\
        {\
          if(g_data.tdr&0x0001)\
            __MY_SET_PIN(GPIOB,GPIO_PIN_10);\
          else\
            __MY_RESET_PIN(GPIOB,GPIO_PIN_10);\
          g_data.tdr>>=1;g_data.tdr_left_bit--;\
        }\
        set_channel_after_tick(0,VUART_BIT_TICK);\
      }

函数 set_channel_after_tick 的作用是设置通道在指定tick之后产生中断。

/**r{
 * fun: 把比较寄存器值设置为tick时间之后
 * par: channel:0~3
 * return: 
}c**/
static void set_channel_after_tick(int channel,int tick)
{
  int t1=__MY_TIM_GET_COUNTER(__MY_TIM);
  if(t1+tick>TIM_CNT_MAX)
    __MY_TIM_SET_COMPARE(__MY_TIM,channel,t1+tick-TIM_CNT_MAX);
  else
    __MY_TIM_SET_COMPARE(__MY_TIM,channel,t1+tick);
}

4.init 函数

init函数作用如下:
1.初始化缓冲区
2.初始化定时器并打开通道1,2
3.初始化引脚PB10为输出
4.初始化引脚PB11为下降沿触发中断
5.打开TIM和EXIT的全局中断

static TIM_HandleTypeDef g_tim;
static int init(void)
{
  if(g_data.inited) return 0;
  buff_init(&g_data.sbuff,200,0,0,0);
  buff_init(&g_data.rbuff,200,0,0,0);
  
  TIM_Base_InitTypeDef *init=&g_tim.Init;
  g_tim.Instance=__MY_TIM;
  init->Prescaler=0;
  init->CounterMode=TIM_COUNTERMODE_UP;
  init->Period=TIM_CNT_MAX;
  init->ClockDivision=TIM_CLOCKDIVISION_DIV1;
  init->RepetitionCounter=0;
  init->AutoReloadPreload=TIM_AUTORELOAD_PRELOAD_ENABLE;
  __HAL_RCC_TIM2_CLK_ENABLE();
  HAL_TIM_Base_Init(&g_tim);
  __MY_TIM_ENABLE(__MY_TIM);
  
  HAL_NVIC_SetPriority(TIM2_IRQn, 3, 1);
  HAL_NVIC_EnableIRQ(TIM2_IRQn);

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  __HAL_RCC_GPIOB_CLK_ENABLE();
 
  /*r{ PB10->TX;PB11->RX }c*/
  GPIO_InitStruct.Pin = GPIO_PIN_10;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
  GPIO_InitStruct.Pin = GPIO_PIN_11;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
  __MY_SET_PIN(GPIOB,GPIO_PIN_10);
  __MY_SET_PIN(GPIOB,GPIO_PIN_11);

  HAL_NVIC_SetPriority(EXTI15_10_IRQn, 3, 1);
  HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
  
  /*r{ 打开通道0,1 }c*/
  __MY_TIM->CCER=0x0011;
  g_data.inited=1;
  return 0;
}

5.其他接口函数

write函数与普通串口的中断发送操作类似,先把数据全部存入缓冲区,设置发送寄存器,然后打开发送中断即可;
read函数只需读取缓冲区即可,与普通串口相同;
set_irqfun函数设置中断回调,也与普通串口相同。

static int write(const uint8_t *d,int len)
{
  uint8_t res;
  buff_save_bytes(&g_data.sbuff,d,len);
  if(g_data.in_send==0)
  {
    g_data.in_send=1;
    if(buff_read_byte(&g_data.sbuff,&res)==0)
    {
      __VUART_SET_DR(res);
      __MY_TIM_ENABLE_IT(__MY_TIM,TIM_IT_CC1);
    }
  }
  return len;
}
static int read(uint8_t *d,int len)
{
  if(buff_read_bytes(&g_data.rbuff,d,len)==0)
    return len;
  else
    return 0;
}
static void *set_irqfun(void (*fun)(uint8_t d,void *context),void *context)
{
  void *r=g_data.irqfun;
  g_data.irqfun=fun;
  g_data.context=context;
  return r;
}

6.中断函数

串口发送只需要用到定时器中断,而接收则需要同时用到外部中断和定时器中断。
发送中断由write函数调用 __VUART_SET_DR ,__MY_TIM_ENABLE_IT(__MY_TIM,TIM_IT_CC1) 开启,然后在定时器中断中依次逐bit发送,1 byte发送完成之后检测发送缓冲区中是否还有数据,如有,则再次调用 __VUART_SET_DR 重复以上流程;如没有数据,则调用 __MY_TIM_DISABLE_IT(__MY_TIM,TIM_IT_CC1) 关闭发送中断,此时发送结束;
接收中断由EXTI触发,一旦检测到下降沿,在EXTI中断服务函数中调用 __VUART_RX_DR ,__MY_TIM_ENABLE_IT(__MY_TIM,TIM_IT_CC2),打开接收中断,调用 __MY_EXIT_DISABLE_IT 关闭EXTI中断防止在数据接收过程中重复触发,然后在定时器中断中依次逐bit接收,1 byte接收完成后存入接收缓冲区,然后调用 __MY_TIM_DISABLE_IT(__MY_TIM,TIM_IT_CC2) 关闭接收中断,调用 __MY_EXIT_ENABLE_IT 打开EXTI中断方便下次接收。

void TIM2_IRQHandler(void)
{
  uint8_t res;
  if(__MY_TIM_GET_IT_SOURCE(__MY_TIM,TIM_IT_CC1)&&__MY_TIM_GET_FLAG(__MY_TIM, TIM_FLAG_CC1))
  {
    __VUART_TX_BIT();
    if(g_data.tdr_left_bit==0)
    {
      if(buff_read_byte(&g_data.sbuff,&res)==0)
      {
        __VUART_SET_DR(res);
      }
      else
      {
        __MY_TIM_DISABLE_IT(__MY_TIM,TIM_IT_CC1);
        g_data.in_send=0;
      }
    }
    
    __MY_TIM_CLEAR_FLAG(__MY_TIM, TIM_FLAG_CC1);
  }
  if(__MY_TIM_GET_IT_SOURCE(__MY_TIM,TIM_IT_CC2)&&__MY_TIM_GET_FLAG(__MY_TIM, TIM_FLAG_CC2))
  {
    __VUART_RX_BIT();
    if(g_data.rdr_left_bit==0)
    {
      buff_save_byte(&g_data.rbuff,g_data.rdr);
      __MY_TIM_DISABLE_IT(__MY_TIM,TIM_IT_CC2);
      __MY_EXIT_ENABLE_IT(11);
      if(g_data.irqfun) g_data.irqfun(res,g_data.context);
    }
    __MY_TIM_CLEAR_FLAG(__MY_TIM, TIM_FLAG_CC2);
  }
}

void EXTI15_10_IRQHandler(void)
{
  if(__HAL_GPIO_EXTI_GET_FLAG(GPIO_PIN_11))
  {
    __VUART_RX_DR();
    __MY_TIM_CLEAR_FLAG(__MY_TIM, TIM_FLAG_CC2);
    __MY_TIM_ENABLE_IT(__MY_TIM,TIM_IT_CC2);
    __MY_EXIT_DISABLE_IT(11);
    __HAL_GPIO_EXTI_CLEAR_FLAG(GPIO_PIN_11);
  }
}

7.接口导出

封装在vuart.h中定义的串口类,导出接口
用户只需使用vuart()->init()来初始化,使用vuart()->write()来发送数据,使用vuart()->read()来接收数据即可。

static vuart_typedef vuart_def=
{
  .init=init,
  .write=write,
  .read=read,
  .set_irqfun=set_irqfun,
};


vuart_typedef *vuart(void)
{
  return &vuart_def;
}


五、验证

在程序线程中编写类似代码:


 void main(void)
 {
 	vuart()->init();
 	while(1)
 	{
    	vuart()->write((uint8_t *)"this msg sent by vuart.\r\n",25);
    	while(vuart()->read((uint8_t *)&vuart_d,1))
      		vuart()->write((uint8_t *)&vuart_d,1);
      	delay(1000);
   	}
}

经验证可以实现数据发送和接收,但由于未经过大量实际运用,可能还存在一些潜在的问题,不过用整活的眼光来看已经成功了。

串口实验结果

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值