在STM32F0上使用软件UART

俗话说的好,能用软件解决的硬件问题就不是硬件问题。这一次研究软件UART并不是因为用到的芯片硬件串口数量不够,而是因为在画PCB的时候,我又双叒叕把Tx和Rx画反了…解决方法就是使用软件模拟UART,避开硬件的Rx和Tx连接错误。

就像其他任何的软件模拟的通信协议一样,软件模拟UART同样是直接对IO口进行操作而不是对外设的寄存器进行操作的。但是软件模拟UART与模拟其他通信协议不一样的地方在于UART是一种异步串口,因此对于时钟的要求较高,不像其他的通信协议例如SPI,不管操作IO口的代码占用多少周期,只要时钟线能够按照正确的时序进行操作,就不会出现通信错误。 所以,对于接收来说,这里使用定时器作为时钟基准:使用定时器中断产生波特率也就是接收一位的时基,同时使用外部中断检测下降沿,接收串口的起始位。发送就相对来说简单一些,直接使用IO口操作,配合延时产生时序。 总结一下,外设的初始化也就是:

  • Tx GPIO设置为输出推挽模式,Rx GPIO设置成输入上拉模式
  • 定时器根据波特率配置好更新中断
  • Rx GPIO启用下降沿中断

初始化代码如下(一些初始化中的外设考虑到移植的便利性都使用了宏进行封装,具体的请自行下载工程查看): 串口发送为简单的阻塞顺序执行程序,在此不过多介绍。直接贴上代码如下:

void SWUART_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct;
    EXTI_InitTypeDef EXTI_InitStruct;
  TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
  NVIC_InitTypeDef NVIC_InitStructure;
  
  RCC_AHBPeriphClockCmd(SWUART_GPIO_RCC_INIT,ENABLE);
  SWUART_TIM_RCC_INIT_CMD();
  
  GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;//TXD 输出推挽
  GPIO_InitStruct.GPIO_Pin = 1<<SWUART_TXD;
  GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
  GPIO_Init(GPIOA,&GPIO_InitStruct);
  
  GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN;//RXD 输入上拉
  GPIO_InitStruct.GPIO_Pin = 1<<SWUART_RXD;
  GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
  GPIO_Init(GPIOA,&GPIO_InitStruct);
  
  TIM_TimeBaseStructure.TIM_Period = SystemCoreClock/SWUART_BAUDRATE - 1;//根据波特率和系统时钟计算定时器的定时周期
  TIM_TimeBaseStructure.TIM_Prescaler = 0;
  TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
  TIM_TimeBaseInit(SWUART_TIM, &TIM_TimeBaseStructure);
  TIM_ClearITPendingBit(SWUART_TIM, TIM_FLAG_Update);
  TIM_ITConfig(SWUART_TIM,TIM_IT_Update,ENABLE );//允许更新中断
  SYSCFG_EXTILineConfig(SWUART_GPIO_EXTI_PINSOURCE,SWUART_RXD);
  EXTI_InitStruct.EXTI_Line = 1<<SWUART_RXD;
  EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
  EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;//下降沿触发中断
  EXTI_InitStruct.EXTI_LineCmd = ENABLE;
  EXTI_Init(&EXTI_InitStruct);
  
  //中断优先级NVIC设置
  NVIC_InitStructure.NVIC_IRQChannel = SWUART_TIM_IRQ_CH;//定时器中断
  NVIC_InitStructure.NVIC_IRQChannelPriority = 2;//优先级较低
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
  NVIC_InitStructure.NVIC_IRQChannel= SWUART_GPIO_EXTI_IRQ_CH;//RXD引脚外部中断
  NVIC_InitStructure.NVIC_IRQChannelPriority = 1;//优先级较高
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
}
void SWUART_SendByte(uint8_t dat)
{
  uint8_t i = 0;
  
  SWUART_TXD_0();  
  SWUART_BIT_DELAY();//起始位
  
  for(i = 0; i < 8; i++)//8位数据位
  {
    if(dat & 1)
    {
      SWUART_TXD_1();
    }
    else
    {
      SWUART_TXD_0();
    }
    SWUART_BIT_DELAY();
    dat >>= 1;
  }
  
  SWUART_TXD_1();  
  SWUART_BIT_DELAY();//停止位
}

接收部分,使用了一个简单的状态机,用来标记当前接接受的状态。这个状态机包括了10种状态,分别是8个数据位以及起始位、停止位。

enum{
  SWUART_BITSTAT_START = 0,
  SWUART_BITSTAT_B0 = 1,
  SWUART_BITSTAT_B1 = 2,
  SWUART_BITSTAT_B2 = 3,
  SWUART_BITSTAT_B3 = 4,
  SWUART_BITSTAT_B4 = 5,
  SWUART_BITSTAT_B5 = 6,
  SWUART_BITSTAT_B6 = 7,
  SWUART_BITSTAT_B7 = 8,
  SWUART_BITSTAT_STOP = 9,
};

当前状态是停止位的时候,若产生了Rx引脚上的下降沿中断,就认为串口接收开始,开启定时器,并将状态更改为起始位。在定时器中断中,若当前是起始位,就按顺序接收8位数据,若接收完8位后,标记当前接收完成、将接收到的数据存入缓冲区,并将状态机更改为停止位,等待下一次接收。

void SWUART_GPIO_EXTI_IRQHANDLER(void)
{
  if(EXTI_GetFlagStatus(1<<SWUART_RXD) != RESET)
  {
    if(SWUART_BitStat == SWUART_BITSTAT_STOP)//如果当前为停止状态 说明接收到的是起始位
    {
      SWUART_BitStat = SWUART_BITSTAT_START;//标记起始位
      TIM_SetCounter(SWUART_TIM,0);
      TIM_Cmd(SWUART_TIM,ENABLE);//启动定时器
    }
    EXTI_ClearITPendingBit(1<<SWUART_RXD);
  }
}
void SWUART_TIM_IRQHANDLER(void)
{
  static uint8_t recvData = 0;
  
  if(TIM_GetFlagStatus(SWUART_TIM, TIM_FLAG_Update) != RESET)
  {
    TIM_ClearITPendingBit(SWUART_TIM, TIM_FLAG_Update);//清定时器中断标志位
    
    SWUART_BitStat ++;      
    if(SWUART_BitStat == SWUART_BITSTAT_STOP)//接收完成
    {
      SWUART_RxBuff[SWUART_RxCnt] = recvData;//接收数据写入缓冲区
      SWUART_RxCnt ++;
      if(SWUART_RxCnt >= SWUART_RXBUFF_SIZE)
        SWUART_RxCnt = 0;
      
      TIM_Cmd(SWUART_TIM,DISABLE);//关闭定时器
      return;//接收到停止位时不执行接收到数据时的位操作
    }
    
    recvData >>= 1;//将接收到的位存入缓冲
    if(SWUART_RXD_IN)
    {
      recvData |= 0x80;
    }
  }

有的人可能会问,如果接收出错了会怎么办?答案很简单。这个状态机只是负责接收起始位然后循环10个定时器周期接受完数据,就会恢复到初始状态。所以即使出现丢位、错位,也会在下一个周期恢复正常。

主程序里,实现了一个简单的接收到01 FE的正反码就开启LED,02 FD就关闭LED的小程序来验证通信的正确性。经过试验,完全可以满足低波特率下的通讯。实测接收可以到115200波特率,但是由于STM32F0的SYSTICK定时器的精度所限制,以及延时函数封装所带来的精度损失,这一个程序的发送部分最高只能到9600波特率,再高会因为延时精度问题导致通讯失败。

要解决这个问题也很简单,第一个方法就是使用汇编语言计算机器周期进行精准延时;第二个方法是同样使用定时器进行发送。如果和接收定时器公用一个定时器,还可以节约一个定时器的消耗,后期我会更新文章来研究这两种方法。

 STM32工程