前言:
串口是很重要的,有许多模块通信接口就是串口,例如gps模块,蓝牙模块,wifi模块还有一些精度比较高的陀螺仪模块等等,所以学会了串口之后,这些听起来很牛批的模块都能够用起来了。此外,单片机的之间的通信,也大多用串口,如距离比较长的RS485,RS232,光纤通信等等有线通信,也只是电平转换芯片不一样,但是代码层面完成是一样的,作为单片机开发串口是很必要熟练的。在学习的第二阶段,尽量还是照着手册来编写代码,或者说,根据自己的思路来嫖代码,而不是像初次学习一样代码、思路都嫖别人的。这样才能最大限度的检验自己的能力,当然,做项目怎样都成,怎么方便怎么来。
思路:
下面就来记录记录我个人的编码思路,首先由下图可以看到,串口的模式还是挺多的:
这样相应的寄存器也就必然很多,所以从一开始就需要明确我们需要的是哪种模式,然后就只关注这个模式,与之无关的寄存器都可以忽略,如此编码就简单清晰了。以最常用的异步模式为例:
手册没有讲初始化流程,所以只能按照经验来写代码了,回忆串口无非就是:串口时钟使能,配置数据位,停止位等,配置波特率,使能串口,从寄存器读出数据/向寄存器写入数据。一般为了方便数据处理,还加一个接收中断。但是~串口不只是串口,还涉及GPIO初始化,GPIO复用配置。
1.初始化GPIO相关配置
三部曲:时钟,IO,复用啥
GPIOx->AFR这个寄存器就是将某个GPIO管脚复用成指定功能的。下面AF虽多,但是要根据数据手册引脚说明来选,芯片没有设计的当然选了也没用。我没有在手册找到AF对应的是什么,不过正点原子的代码有写,也不知哪里找的。
//AF0~15设置情况(这里仅是列出常用的,详细的请见407数据手册,56页Table 7):
//AF0:MCO/SWD/SWCLK/RTC AF1:TIM1/TIM2; AF2:TIM3~5; AF3:TIM8~11
//AF4:I2C1~I2C3; AF5:SPI1/SPI2; AF6:SPI3; AF7:USART1~3;
//AF8:USART4~6; AF9;CAN1/CAN2/TIM12~14 AF10:USB_OTG/USB_HS AF11:ETH
//AF12:FSMC/SDIO/OTG/HS AF13:DCIM AF14: AF15:EVENTOUT
编码如下:
1.时钟
RCC->AHB1ENR|=1<<0; //GPIOA时钟附属于AHB1
2.IO
GPIO_Set(GPIOA,PIN9|PIN10,GPIO_MODE_AF,0,0,0);//PA9,PA10,都配置为复用模式,其他电气属性如上下拉之类的可以根据需要配置,不配置也行的。
3.复用啥,直接用正点原子的函数,里面其实就是对GPIOx->AFRH和GPIOx->AFRL这两个寄存器进行编写,不过正点原子这个封装的挺好的,一目了然
GPIO_AF_Set(GPIOA,9,7); //PA9,AF7
GPIO_AF_Set(GPIOA,10,7);//PA10,AF7
2.串口相关初始化
目的是:串口时钟使能,配置数据位,停止位,接收中断,使能串口等,配置波特率,从寄存器读出数据/向寄存器写入数据。
由手册第66页可知,USART1时钟隶属于APB2
1.使能串口1时钟
RCC->APB2ENR|=1<<4;
2.配置波特率:
根据公式算,然后填到对应的位里面去
float temp;u16 mantissa; //整数部分u16 fraction; //小数部分temp=(float)(pclk2*1000000)/(bound*16);//得到USARTDIV@OVER8=0mantissa=temp; //得到整数部分fraction=(temp-mantissa)*16; //得到小数部分@OVER8=0 mantissa<<=4;mantissa+=fraction; USART1->BRR=mantissa; //波特率设置
这个是正点原子那嫖的,适用于多种时钟,多种波特率的情况,挺好用的。
3.配置数据位,停止位,中断等
USART_CR1检索:只看和异步通信有关的位,其他的不管
bit[2]: 接收使能
bit[3]: 发送使能
bit[5]: 接收中断使能
bit[10]: 关/开奇偶检验
bit[12]: 置零:1 起始位,8 数据位,n 停止位
bit[13]: 串口关闭/使能,后面记得给这个串口中断分组以及设置优先级
bit[15]: 0:16倍过采样率,1:8倍过采样率,这个是和波特率计算有关的,设为0
其他就无所谓了,好像这个寄存器就完全够配置我们所需了
USART1->CR1 = 0<<15 | 1<<13 | 0<<12 | 0<<10 | 1<<5 | 1<<3 | 1<<2 ;
//中断分组及优先级,中断后面有时间再讲(其核心思想就是分组,中断线,设优先级三部曲)
MY_NVIC_Init(3,3,USART1_IRQn,2);//组2,最低优先级
4.完整的串口初始化代码如下:
void uart_init(u32 pclk2,u32 bound)
{ float temp;u16 mantissa;u16 fraction; //1.GPIO初始化相关RCC->AHB1ENR|=1<<0; //使能PORTA口时钟 GPIO_Set(GPIOA,PIN9|PIN10,GPIO_MODE_AF,0,0,0);//PA9,PA10,复用功能GPIO_AF_Set(GPIOA,9,7); //PA9,AF7GPIO_AF_Set(GPIOA,10,7);//PA10,AF7 //2.使能串口1时钟 RCC->APB2ENR|=1<<4; //3.波特率设置temp=(float)(pclk2*1000000)/(bound*16);//得到USARTDIV@OVER8=0mantissa=temp; //得到整数部分fraction=(temp-mantissa)*16; //得到小数部分@OVER8=0 mantissa<<=4;mantissa+=fraction; USART1->BRR=mantissa; //波特率设置 //4.配置数据位,停止位,中断等USART1->CR1 = 0<<15 | 1<<13 | 0<<12 | 0<<10 | 1<<5 | 1<<3 | 1<<2 ;//中断分组及优先级,中断后面有时间再讲(其核心思想就是分组,中断线,设优先级三部曲)MY_NVIC_Init(3,3,USART1_IRQn,2);//组2,最低优先级
}
运行效果:
其他代码不改动,换上自己思路写的代码运行ok:
3.中断服务函数
a.它最原始的模样:
void USART1_IRQHandler(void)
{u8 res; if(USART1->SR&(1<<5))//接收到数据的标志置1---->>有数据{ res=USART1->DR; //取出接收到的数据---->>1B} }
如果要发送数据,可以编写如下:
u8 res;
USART1->DR = res;//要发送的数据 1B
while((USART1->SR&0X40)==0);//等待发送结束
b.正点原子给的:
u8 USART_RX_BUF[USART_REC_LEN]; //里面存着接收到的数据u16 USART_RX_STA=0; //是否有数据标志+接收到的字节数//下面这个不用改它,原封不动放代码里就能用
void USART1_IRQHandler(void)
{u8 res; if(USART1->SR&(1<<5))//接收到数据{ res=USART1->DR; if((USART_RX_STA&0x8000)==0)//接收未完成{if(USART_RX_STA&0x4000)//接收到了0x0d{if(res!=0x0a)USART_RX_STA=0;//接收错误,重新开始else USART_RX_STA|=0x8000; //接收完成了 }else //还没收到0X0D{ if(res==0x0d)USART_RX_STA|=0x4000;else{USART_RX_BUF[USART_RX_STA&0X3FFF]=res;USART_RX_STA++;if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收 } }} } }
其中:
判断有无数据接收:
if(USART_RX_STA&0x8000){。。。。。。}
得知数据共有多少B:
int len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度
数据存放的数组:
USART_RX_BUF[]
相对来说以及挺方便的了~下面还有一个比赛常用的,自定义的协议
c.自定义协议:
首先在usart.c中加入变量:
/*更改变量 BEGIN-- */
uint8_t uart1_rxbuff;//引入该.h可使用
uint8_t uart2_rxbuff;//引入该.h可使用
uint8_t uart3_rxbuff;//引入该.h可使用uint8_t sendBuf[1];
u8 uart1_sdbuffer[11]={0x2c,0x12,0x11,0x22,0x33,0x5b,0,0,0,0};//从索引2开始赋值
/*更改变量 END-- */
在usart.h中导出方便别的文件使用:
extern uint8_t uart1_rxbuff;
extern uint8_t uart2_rxbuff;
extern uint8_t uart3_rxbuff;extern uint8_t uart1_sdbuffer[11];
下面是协议解析函数,自定义的协议是:
协议头0x2c,0x12
协议尾0x5b,想要让协议数据位变多,只需要修改变量RxBuffer1[]的定义即可
//解析接收的数据 最多11哥,两个帧头,一个帧尾,其他是数据位
void Portocol_Receive_Data(uint8_t com_data)
{uint8_t i;static uint8_t RxCounter1=0;//计数static uint8_t RxBuffer1[11]={0};static uint8_t RxState = 0; static uint8_t RxFlag1 = 0;u8 pi=0;//printf("%x\t",com_data);//打印调试if(RxState==0&&com_data==0x2C) //0x2c帧头 RxCounter1==1{RxState=1;RxBuffer1[RxCounter1++]=com_data; }else if(RxState==1&&com_data==0x12) //0x12帧头 RxCounter1==2{RxState=2;RxBuffer1[RxCounter1++]=com_data;}else if(RxState==2)//开始接收数据位{ RxBuffer1[RxCounter1++]=com_data;if(RxCounter1>=10||com_data == 0x5B){//RxCounter1-1是帧尾if(RxBuffer1[RxCounter1-1] == 0x5B)//接收到贞结尾了{/* USER CODE BEGIN 2 */
// for(i = 0; i <= 10; i++)
// {
// printf("%x\t",RxBuffer1[i]);
// }
// printf("\r\n");USART1_Portocol_Send_Data();
// printf("\r\n");/* USER CODE END 2 */RxFlag1 = 0;RxCounter1 = 0;RxState = 0;}else //接收错误{RxState = 0;RxCounter1=0;for(i=0;i<11;i++){RxBuffer1[i]=0x00; //将存放数据数组清零}}}}else //接收异常{RxState = 0;RxCounter1=0;for(i=0;i<10;i++){RxBuffer1[i]=0x00; //将存放数据数组清零}}
}
中断服务函数是这样滴:
void USART1_IRQHandler(void)
{if(USART1->SR&(1<<5))//接收到数据{ uart1_rxbuff = USART1->DR;Portocol_Receive_Data(uart1_rxbuff);}
}
另外还有一个协议配套的发送函数:
要修改发送的内容只需修改uart1_sdbuffer数组的内容即可:
//串口X发送函数
void USART1_Portocol_Send_Data(void)
{u8 i;for(i = 0; i <= 10; i++){USART1->DR=uart1_sdbuffer[i];//要发送的数据 1Bwhile((USART1->SR&0X40)==0);//等待发送结束}
}
效果如下:
在协议代码中,下面这部分就是给你自由发挥的,进到这段代码里说明成功接收到了按协议格式发来的信息;
4.拓展到其他串口:
复用到其他的串口也很简单,仿照把发送呀接收呀里面的寄存器改一改就行了
比赛常用的还是自定义协议的串口,比如双车用蓝牙通讯呀,或者stm32和openmv通讯,几乎都要自己写一个协议去收发数据,这样才会可靠。
完~