在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波特率,再高会因为延时精度问题导致通讯失败。
要解决这个问题也很简单,第一个方法就是使用汇编语言计算机器周期进行精准延时;第二个方法是同样使用定时器进行发送。如果和接收定时器公用一个定时器,还可以节约一个定时器的消耗,后期我会更新文章来研究这两种方法。