STM32 串口功能详解
姓名:周崇杰 学号:16140120059 专业:机械设计制造及其自动化
转载自:http://blog.csdn.net/andylfg/article/details/24393419,有删节
【嵌牛导读】:单片机与计算机系统最关键的莫过于信息与数据,单片机最有用的通信工具莫过于串口,本文将会对STM32的串口以及其库函数做出详解。
【嵌牛鼻子】:STM32单片机,串口,库函数
【嵌牛提问】:数据传输时要从支持那些相关的标准?传输的速度?什么时候开始?什么时候结束?传输的内容?怎样防止通信出错?数据量大的时候怎么弄?
【嵌牛正文】:
数据传输时要从支持那些相关的标准?传输的速度?什么时候开始?什么时候结束?传输的内容?怎样防止通信出错?数据量大的时候怎么弄?硬件怎么连接出发,当然对于stm32还要熟悉库函数的功能
具起来rs232和485电平的区别硬件外围芯片,波特率(反映传一位的时间),起始位和停止位,数据宽度,校验,硬件流控制,相应连接电脑时的接口怎么样的。配置,使用函数,中断,查询并结合通信协议才算了解了串口使用。
以上是基础,当然stm很多相关复用功能,支持同步单向通信和半双工单线通信,支持局部互联网、智能卡协议和红外数据组织相关规范,以及调制解调器操作,运行多处理器通信。同时可以使用DMA方式进行高速数据通信。注意Print函数时间问题,尝试通过DMA解决。
特点:全双工,异步,分数波特率发生器好处是速度快,位数8或9为,可配置1或2停止位,Lin协议,可提供同步时钟功能。
硬件上
一般2个脚,RX和TX;同步模式需要SCLK时钟脚,红外IRDA需要irDA_RDI脚作为数据输入和irDA_TDO输出。
奇偶校验通过usart_cr1pce位配置校验(发送生成奇偶位,接受时进行校验)
LIN局域互联网模式:通过设置USART_CR2中LINEN位配置,使用的时候需要外加专门的收发器才可以
同步模式:通过设置USART_CR2中CLKEN位配置
智能卡模式:通过设置USART_CR3中SCEN位配置
DMA、硬件流控制作专门研究。
中断有哪些事件?
发送数据寄存器空发送完成接受数据可读奇偶校验错数据溢出CTS标志 空闲标志 断开标志 噪声标志
遗憾是没有留有接受缓冲区,用查询容易数据丢失
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
流程是时钟配置---管脚配置(如重映射,管脚模式)----串口配置----中断配置-----相应中断-----打开串口
上面是一些基础知识点,下面从实际运用来了解串口功能
比较简单些的应用吧:对usart进行初始化的工作
void COM1_Init( void)
{
//首先要初始化结构体:少不了对于的引脚,忘不了usart,更牵挂着中断的配置结构体,定义空间来被涂鸦
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//下面是对GPIO进行涂鸦
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;//选择管脚位
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//模式复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//选择管脚位
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//模式为输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 115200;//波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8数据位
USART_InitStructure.USART_StopBits = USART_StopBits_1;//1停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//收发模式
USART_Init(USART1, &USART_InitStructure);
//使能串口中断,并设置优先级
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = USART1_IRQn_Priority;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);//将结构体丢到配置函数,即写入到对应寄存器中
//USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
//所有的工作都做好了,最后别忘了打开串口
USART_Cmd(USART1, ENABLE);
}
//USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);下面是中断源
#define USART_IT_PE((uint16_t)0x0028)
#define USART_IT_TXE((uint16_t)0x0727)
#define USART_IT_TC((uint16_t)0x0626)
#define USART_IT_RXNE((uint16_t)0x0525)
#define USART_IT_IDLE((uint16_t)0x0424)
#define USART_IT_LBD((uint16_t)0x0846)
#define USART_IT_CTS((uint16_t)0x096A)
#define USART_IT_ERR((uint16_t)0x0060)
#define USART_IT_ORE((uint16_t)0x0360)
#define USART_IT_NE((uint16_t)0x0260)
#define USART_IT_FE((uint16_t)0x0160)
--------------------------------------------------------------------------------------
以上是初始化配置,下面还要构成最小的运用,就举例输出函数吧
void PrintUart1(const u8 *Str)
{
while(*Str)
{
USART_SendData(USART1, (unsigned char)*Str++);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
}
发送字符是通过查询字符串的状态来来不断的发送下一个数据。
接受数据是通过中断来实现的,把接受的数据放入缓冲区,来实现。有包含协议以后细讲。
想玩电脑串口传输数据,通过printf()来直接在电脑窗口显示是不是很爽?在usart.c函数中加入
//不使用半主机模式
#if 1 //如果没有这段,则需要在target选项中选择使用USE microLIB
#pragma import(__use_no_semihosting)
struct __FILE
{
int handle;
};
FILE __stdout;
_sys_exit(int x)
{
x = x;
}
#endif
int fputc(int ch, FILE *f)
{
USART_SendData(USART1, (unsigned char)ch);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
return ch;
}
上面数据来源可以通过串口,通过usb,通过无线等等。。。
printf函数有个缺陷,就是花费的时间太多了(毫秒级),总不至于让CPU等几个毫秒就来显示串口吧,那再好的CPU也就费了,那肿么办?可以用DMA!!直接让其它硬件来传这些粗糙的工作。CPU玩重点的其它的活!
先接着讲好串口接受,再说这个DMA,在固件库里面有个文件是专门用来放中断处理函数的
里面有个函数
void USART1_IRQHandler(void)
{
static u8 UartBuf[UART_BUF_LEN];//串口缓冲
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
temp=USART_ReceiveData(USART1);
..............下面就是一些处理,可以用状态机模式来玩,直到填充好串口缓冲数据,并校验正确,不多说
}
}
上面是典型的中断处理函数,结合状态机功能就强大了。讲到操作系统还要进一步的学会运用。
---------------------------------------------------------------------------------------------
通过上面的学习相信对基本的串口操作有了比较深入的理解了,前面提到printf太慢,这里需要一些改进。
考虑到真正执行串口输出(用DMA其实在几毫秒后完成)比执行完pruntf(立即完成)这个时间点晚个几毫秒对实际应用来说没有任何影响,因此CPU可以在嘀嗒中断中指挥DMA模块完成串口输出的任务。(其实对系统还是有些影响的宏观讲,串口输出的数据其实很少,在大河中放入一杯水,几乎可以忽略不计的)
再解决怎么分配:
定义两个全局缓存区
其中一个缓存区以循环队列的形式组织,每次执行fputc时向其队尾加入一个元素。
另一个缓存区直接以数组的形式组织,作为DMA的源地址。
在嘀嗒中断中按下列顺序完成对DMA的操作
(1)判断循环队列是否为空,如果为空说明当前没有字符串需要通过串口输出直接跳至(6)
(2)判断DMA是否正在工作,如果DMA正在工作说明上次分配的任何没干完直接跳至(6)
(3)从循环队列出队N个字符到数组缓存
(4)告诉DMA本次需传输的字节数N
(5)命令DMA开始传输
(6)结束操作
补充:
1.N的确定方法:若循环队列中元素的个数大于或等于数组缓存区的长度(固定值),则将数组缓存区的
长度赋给N,若循环队列中元素的个数小于数组缓存区的长度则将循环队列元素的个数赋给N
2.循环队列开辟得越大,能缓存的字符串就越多,因此是越大越好.
3.数组缓存区并不是开辟的越大越好,这个值可以做如下的计算得出,假设波特率为115200嘀嗒中断
的周期为2毫秒则在这2毫秒时间内理论上最多可传115200*0.002/10=23个字节。因此把数组缓存区
的大小定到比23稍小一点即可,比如定为20.
代码可正常运行。经测试使用新方案printf一个包含了二十个字符的字符串只需要25微秒的CPU耗时,
而老方案则需要1.76毫秒的CPU耗时。从而可以放心的使用printf调试一些时序要求较高的函数了,
另外因为执行时间段从而printf被重入的概率大大减小。如果需要彻底防止printf被重入的话,可在调用printf之前关中断,在printf执行完之后开中断,代价也仅是可能发生的几十微秒的中断延时而已。
.........................................................................................
下面讲一讲我对DMA的理解
stm32DMA有8通道,0---7
既然DMA传输的是数据,当然有数据的宽度了,这需要配置。另外还要有地址吧,从哪个地址开始传,还有传到哪个地址,需要配置。还有传输普通的就直接传完就好了,如果要高速不断循环传输,这也可以配置。还有从ROM直接快速的加载到RAM(反过来不可以)也可以的。
之上就是一些基本需要配置的工作
DMA_DeInit(DMA1_Channel4);
上面这句是给DMA配置通道,根据ST提供的资料,STM3210Fx中DMA包含7个通道
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&((USART_TypeDef *)USART1)->DR);
上面是设置外设地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) USART_DMA_BUF;
上面这句很显然是DMA要连接在Memory中变量的地址,上面设置存储器地址;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
上面的这句是设置DMA的传输方向,就如前面我所说的,从存储器到外设,也可以从外设到存储器,:把DMA_DIR_PeripheralSRC改成DMA_DIR_PeripheralDST即可。
DMA_InitStructure.DMA_BufferSize = 0;
上面的这句是设置DMA在传输时缓冲区的长度,前面有定义过了buffer的起始地址:为了安全性和可靠性,一般需要给buffer定义一个储存片区,这个参数的单位有三种类型:Byte、HalfWord、word,我设置的2个half-word(见下面的设置);32位的MCU中1个half-word占16 bits。
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
上面的这句是设置DMA的外设递增模式,如果DMA选用的通道(CHx)有多个外设连接,需要使用外设递增模式:DMA_PeripheralInc_Enable;我的例子选用DMA_PeripheralInc_Disable
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
上面的这句是设置DMA的内存递增模式,DMA访问多个内存参数时,需要使用DMA_MemoryInc_Enable,当DMA只访问一个内存参数时,可设置成:DMA_MemoryInc_Disable。
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
上面的这句是设置DMA在访问时每次操作的数据长度。有三种数据长度类型,前面已经讲过了,这里不在叙述。
DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte;
与上面雷同。在此不再说明。
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
上面的这句是设置DMA的传输模式:连续不断的循环模式,若只想访问一次后就不要访问了(或按指令操作来反问,也就是想要它访问的时候就访问,不要它访问的时候就停止),可以设置成通用模式:DMA_Mode_Normal
DMA_InitStructure.DMA_Priority = DMA_Priority_Low;
上面的这句是设置DMA的优先级别:可以分为4级:VeryHigh,High,Medium,Low.
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
上面的这句是设置DMA的2个memory中的变量互相访问的
DMA_Init(DMA_Channel1,&DMA_InitStructure);
前面那些都是对DMA结构体成员的设置,在次再统一对DMA整个模块做一次初始化,使得DMA各成员与上面的参数一致。
DMA_Cmd(DMA_Channel1,ENABLE);
ok上面的配置工作完成了,相当于设定了一根管道通过DMA把缓冲区中要发送的数据发送到串口中。当然要使得DMA与usart1相连接,在usart1中还要把usart的DMA功能打开:USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);接着在程序中只需要监控,数据发送的状态,并适时的打开或关闭DMA,留着下一次的串口发送用。
函数重载的printf()函数如下
int fputc(int ch, FILE *f)
{
while(En_Usart_Queue(ch)==-1);
return ch;
}
起始就是往环形缓冲区中添加要串口打印的数据,这个动作是比较快的。因为cpu直接移动数据很快,而通过cpu来操作串口,等待收获反映,有个while(..)是比较慢得,故有几个毫秒的延时。现在好了,cpu ,通过printf只是移动数据到了缓冲区,缓冲区在一定的时候,cpu指挥dma来开始接下来的操作,然后dma操作,cpu接着做其他的事情,仅仅只要在下次空闲的时候来查一下dma有木有传输完成,或者有没有传输错误,并改变环形队列首位的位置,以及更改相应的状态就可以了。这样可以大大的节省很多的时间哦。
环形队列的结构,大家可以看一些算法,不是很难的。
下面是运行过程中,cpu操作查询的函数。
void DMA_USART_Handler(void){
u16 num=0;
s16 read;
if(DMA_GetCurrDataCounter(DMA1_Channel4)==0){ //检查DMA是否完成传输任务
DMA_Cmd(DMA1_Channel4, DISABLE);
while((read=De_Usart_Queue())!=-1){
USART_DMA_BUF[num]=read;
num++;
if(num==USART_DMA_BUF_SIZE)
break;
}
if(num>0){
((DMA_Channel_TypeDef *)DMA1_Channel4)->CNDTR = num;//数量寄存器清零
DMA_Cmd(DMA1_Channel4, ENABLE);
}
}
}