一、整活说明
在帮别人做项目的时候,写着代码唱着歌,突然就遇到硬件工程师把线连错了,串口模块连接的不是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_TX | PB10 | UART_RX | PB11 | 定时器 | 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
#define TIM_FREQUENCY (84)
#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__))
#define __MY_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) \
(*((&(__HANDLE__)->CCR1)+(__CHANNEL__)) = (__COMPARE__))
#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__)
#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;
uint8_t rdr_left_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);}
#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);}
#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之后产生中断。
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();
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);
__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);
}
}
经验证可以实现数据发送和接收,但由于未经过大量实际运用,可能还存在一些潜在的问题,不过用整活的眼光来看已经成功了。
|