一、通信方式相关
1.1 并行通信
1.2 串行通信
串行通信的通信方式:
常见的串行通信接口:
STM32的串口通信接口
- USART:通用异步收发器
- UART:通用同步异步收发器
- STM32F10x大容量系列芯片,包含3个USART(支持异步也支持同步)和2个UART(只支持异步)
二、UART 异步通信方式引脚连接方法:
- RXD:数据输入引脚,数据接收
- TXD:数据输出引脚,数据发送。
- PC机里CPU也是含有发送引脚和接收引脚的,他们通过主板引出已经被转化为RS232电平,然后通过DP9接口引出的,如果ARM芯片想要和PC机进行通信,需要TxD和RxD通过转换器转化为RS232电平。
STM32F103ZET6芯片的串口资源如下:
三、UART异步通信方式特点
四、串行通信的过程
- 数据通过RXD引脚,一位一位的传输到串行输入移位寄存器,一个字节的数据传输完成后,串行输入移位寄存器的数据一次性传输到输入数据寄存器,然后CPU就可以读这个输入数据寄存器。
- 这里输出数据缓冲器对应的是TDR寄存器,然后TXE的含义是TDR寄存器为空就会产生中断,也就是上图所示输出数据缓冲器TDR的数据并行发到了TSR(移位寄存器),这时TDR寄存器为空,就会产生中断。当TDR中的数据转移到TSR中时TXE置1,如果有数据写入TDR时就能将TXE置0;
- TC位的含义是,设置TC表示当TSR(移位寄存器)发送过数据后就会产生中断,也叫作发送完中断。具体作用是如果SR中的数据全部通过TXD引脚移出去了并且没有数据进入DR,则TC会置1。
- 当TDR中的数据传送到移位寄存器后,TXE被设置,此时移位寄存器开始向TX信号线按位传输数据,但因为TDR已经变空,程序可以把下一个要发送的字节(操作USART_DR)写入TDR中,而不必等到移位寄存器中所有位发送结束。
- 同时TXE或者TC,根据资料和测试的结果,TXE在复位后就是置1的,即在执行
USART_ITConfig(USART1, USART_IT_TXE, ENABLE)
后会立即产生中断请求。因此这造成一个麻烦的问题:如果没有真正的发送数据,TXE中断都会发生,而且没有休止,这将占用很大部分的CPU时间,甚至影响其他程序的运行! 因此建议的是在初始化时不好启用TXE中断。 - TXE只能通过写DR来置0,不能直接将其清0,而TC可以直接将其写1清零。
- RDR和TDR都叫DR,TSR和RSR都叫SR。
五、STM32串口异步通信需要定义的参数
- 起始位
- 数据位(8位或者9位)
- 奇偶校验位(第9位)
- 停止位(1,15,2位)
- 波特率设置
六、波特率计算方法
- 十进制的整数转化为16进制要除以16,十进制的小数转化为16进制的数要乘以16
- 第二种解释:后面四位表示的是小数,所以最大表示到1,从十进制来看它有4个位,最大表示为 2 4 2^4 24,四个位都写1等于最大16,实际表示的值为最大的十进制1,所以最小的精度为 1 16 \frac{1}{16} 161,现在的小数是0.0625,除以最小精度就是需要写进去的值。
七、串口操作相关库函数
串口初始化结构体 USART_InitTypeDef
/**
* @brief USART Init Structure definition
*/
typedef struct
{
uint32_t USART_BaudRate; //设置波特率 BRR
uint16_t USART_WordLength; //设置字长8位还是9位
uint16_t USART_StopBits; //设置停止位
uint16_t USART_Parity; //设置奇偶校验位,或者无校验
uint16_t USART_Mode; //设置发送使能还是接收使能
uint16_t USART_HardwareFlowControl; //设置硬件流控制
} USART_InitTypeDef;
八、串口配置的一般步骤
九、原子串口协议代码
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 Res;
// 数据
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
Res = USART_ReceiveData(USART1); //读取接收到的数据
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;//接收数据错误,重新开始接收
}
}
}
}
}
首先定义了下面几个变量:
#define USART_REC_LEN 200
表示电脑通过上位机一次给开发板发的最大数据量为200个字节。- u8 USART_RX_BUF[USART_REC_LEN]表示把一次从电脑发给开发板的数据存在这个数组里。
- USART_RX_STA(此寄存器其实就是一个全局变量,由作者自行添加。由于它起到类似寄存器的功能,这里暂且称之为寄存器),实现对串口数据的接收管理
- USART_RX_STA 作为一个接收状态寄存器其各的定义如下图所示:
程序要求,发送的字符是以回车换行结束(0x0D,0x0A),当接收到0X0D,bit14被置1,当接收到0X0A,bit15被置1。此时这一次从电脑发送到开发板的数据接收完成。 - 以发送“ABCDEFGHI…(0x0D),(0x0A)”为例,一个字符为一个字节的数据,当一个A字符接收完毕,就会产生一次中断,进入中断服务函数
USART1_IRQHandler()
十、重定向printf函数到串口输出
STM32(ARM处理器)在MDK中不能直接使用printf函数对串口进行数据输出,它需要我们进行一些修改和添加一些程序定义才可以使用。因printf()之类的函数,使用了半主机模式。使用标准库会导致程序无法运行。所以要不使用半主机模式。
Keil C 的标准库stdio.h:标准输入输出头文件 (C语言标准库),其默认输出设备是显示器,要实现在串口或LCD输出,必须重定义标准库函数里调用的与输出设备相关的函数。
重定向:MDK原本目标是PC机的显示器,然后由于重定向,修改了printf的底层函数(重定义),使printf打印到单片机的外设中。
重定义:就是重新再一次的定义函数,使其拥有新的定义,然后完成新的功能的过程。
要在ARM芯片里使用printf打印到串口显示的方法:
方法一:使用微库【MicroLib】
虽然避免了半主机模式,但是开发板没有直接对目标(电脑的)显示器的使用权限,它必须使用外设(串口)发送数据到电脑的串口助手上面才能显示。并且需要重新定向到外设中,重定义printf底层的发送程序。
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0); //循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
注:必须勾选Use MicroLib方框,然后再调用重定义fputc函数。然后就可以使用printf函数发送数据了。
方法二: 关闭半主机模式
1)确保程序中没有链接 C 库半主机函数
#pragma import(__use_no_semihosting)
2)需要支持的标准库文件来消除被提及函数的问题
//支持使用半主机函数的标准库文件
struct __FILE
{
int handle;
};
FILE __stdout;
3)因为使用了半主机函数,而被要求的函数
//重新定义_sys_exit(),消除编译出错的问题
_sys_exit(int x)
{
x = x;
}
4)重定向,让printf输出到串口
int fputc(int ch, FILE *f)
{
USART_SendData(USART1,ch);
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
return ch;
}
注:必须要把四部分写完整才可以。然后串口就可以使用printf发送数据了。
十一、总结
11.1 字长设置
关于USART通信协议的字长设置如下图所示:
对于寄存器CR1的M位,设置有两种:
其中设置为9个数据位,一般表示前8个数据为需要传输的1个字节的数据,后面的第9位用来配置奇偶校验位,而设置为8个数据位一般就不会用到奇偶校验位,直接8个数据位用来传输1个字节的数据。后面的n个停止表示可以配置的停止位的大小,可配置为1、0.5、2、1.5个停止位。
关于上图所示的空闲帧的定义是:被视为完全由 “ 1"组成的一个完整的数据帧,后面跟着包含了数据的下一帧的开始位,也就是要开始串口通信前,先发一个0XFF的空闲帧数据过去,表示要开始串口通信了。
11.2 发送器
根据M位设置的情况,发送使能后,发送移位寄存器里的数据在TX脚上输出。如果是同步收发器的话,相应的时钟脉冲就在CK脚上输出。
按字符发送的时候,在USART发送期间,在TX引脚上首先移出数据的最低有效位,每个字符之前都有一个低电平的起始位;之后跟着的停止位。需要注意的是:①在数据传输期间不能复位TE位,否则将破坏TX脚上的数据,因为波特率计数器停止计数。正在传输的当前数据将丢失。②TE位被激活后将发送一个空闲帧,然后开始串口通信,所以在TE被置位吼,在真正发送开始之前,有一个bit时间的延迟。
11.3 单字节通信
- TEX(发送数据寄存器空)位,是在TDR(发送数据寄存器)的数据并行传到数据移位寄存器之后硬件自动置1的,如果要清零TXE位,只能通过给TDR寄存器写入一个字节数据来清0。
- 数据如果从TDR转移到了移位寄存器,那么表示数据发送就开始了。
- 如果设置了TXEIE为1,那么当TEX被硬件自动置1的时候,会产生一个串口中断。
- 如果此时USART的TX引脚正在发送数据,此时对USART_DR寄存器的写操作会把下一个需要传输的数据存进TDR寄存器,当上一个字节的数据在移位寄存器里一个bit一个bit的传输出去到最后一位结束的时候,就会把TDR里放的下一个数据复制进移位寄存器。
- 如果此时USART没有在发送数据,处于空闲状态,也就是现在准备开始发送第一个数据,那么对USART_DR寄存器的写操作就会直接把数据放进移位寄存器,数据传输开始, TXE位立即被置起。因为现在移位寄存器是没有数据正在传输的,所以TDR寄存器里的数据直接传到移位寄存器里。
- 当TDR的数据被转移到移位寄存器了,TXE位就会被置位,如果移位寄存器里的一帧数据发送完成时(停止位发送后)并且TXE位被置位了, 那么TC位也被硬件自动置1。此时如果USART_CR1寄存器中的TCIE位被置1的话,则会产生中断。
- 断开帧的定义:设置SBK可发送一个断开符号。断开帧长度取决M位。如果设置SBK=1,在完成当前
数据发送后,将在TX线上发送一个断开符号。断开字符发送完成时(在断开符号的停止位时)SBK被硬件自动复位。 USART在最后一个断开帧的结束处插入一逻辑’1’,以保证能识别下一帧的起始位。注意:如果在开始发送断开帧之前,软件又复位了SBK位,断开符号将不被发送。如果要发送两个连续的断开帧, SBK位应该在前一个断开符号的停止位之后置起。
11.4 字符接收
- 在USART接收期间,数据的最低有效位首先从RX脚移进。
- 当一个字符被接收时,RXNE位被置位,表示移位寄存器里的一个字节数据被并行传到了RDR(数据接收寄存器),换句话说,数据已经被接收并且可以被CPU读出(包括与之有关的错误标志)。此时如果RXNEIT是被置1的状态那么会产生一个中断。
- 在单缓冲器模式里,由软件读USART_DR寄存器完成对RXNE位清除。RXNE标志也可以通过对它写0来清除。 RXNE位必须在下一字符接收结束前被清零,以避免溢出错误。
- 注意:在接收数据时, RE位不应该被复位。如果RE位在接收时被清零,当前字节的接收被丢失。
11.5 USART中断请求
十二、串口通信实验
不使用中断之前:串口的初始化函数
void UART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
//开启GPIOA 和 USART1 的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1,ENABLE);
//复位串口
USART_DeInit(USART1);
//配置串口的PA9(TX)
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//配置串口的PA10(RX)
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//配置串口的模式
USART_InitStruct.USART_BaudRate = 115200;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_Init(USART1,&USART_InitStruct);
//使能串口
USART_Cmd(USART1,ENABLE);
//使能串口吼发送一个空闲帧,等待空闲帧发送完毕后将TC标记位清0
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) != SET);
//否则开启TC中断后马上中断
USART_ClearFlag(USART1,USART_FLAG_TC);
}
这里初始化串口的函数最底下的USART_ClearFlag(USART1,USART_FLAG_TC)
是我在编写发送一个数组的数据到串口之后遇到问题②之后加入的。我的主函数如下所示:
int main(void)
{
uint8_t arr[10] = {1,2,3,4,5,6,7,8,9,10};
UART_Config();
UART_SendArray(arr,10);
while(1)
{
}
}
最原始的发送一个数组的数据的函数如下:
//发送一个数组的数据
void UART_SendArray(uint8_t *array,uint8_t num)
{
uint8_t i = 0;
for(i = 0; i < num; i++)
{
USART_SendData(USART1,array[i]);
}
while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET);
}
遇见的问题①:发送的数组是arr[10] = {1,2,3,4,5,6,7,8,9,10}
,但是到串口打印之后使用上面的void UART_SendArray(uint8_t *array,uint8_t num)
函数发送到上位机,只显示了数组的最后一个元素0A。
原因是可能上一个数据还没有发送完成 就开始了下一个数据的写入,导致数据被破坏,只有最后一个数据进行到了等待,使它有足够的时间被发送完成。所以在每个数据发送的过程中都进行等待,等待上一个数据被完全发送出去才开试写入下一个数据。
修改发送数组数据的函数如下:每发送一个数据之后,检测一下标志位。
//发送一个数组的数据
void UART_SendArray(uint8_t *array,uint8_t num)
{
uint8_t i = 0;
for(i = 0; i < num; i++)
{
USART_SendData(USART1,array[i]);
while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET);
}
while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET);
}
这样就解决了第一个问题,紧接着就出现了问题②了,虽然显示了数组的元素,但是发送到串口的数组缺失了第一个元素的数据,效果如下所示:
原因如下:首先看状态寄存器,可以看到复位值为0X00C0,表示串口初始化完成之后TXE和TC的默认值为1。
然后注意到一点区别是:
- 往USART_DR中写数据的时候TXE直接被清0;
- 清零TC位则需要先读取USART_SR,然后再写USART_DR才能完成TC的清零。
然而硬件复位后,串口发送的首个数据之前没有读USART_SR的操作,是直接写USART_DR,也就是说,TC没有被清除掉。 导致第二个数据覆盖了首个数据,使得首个数据丢失
所以硬件复位后,串口发送首个数据之前,先读取一下USART_SR,则能够保证首个数据发送时,不出现覆盖的情况。当然,也有别的方法,比如先清除TC状态位,USART_ClearFlag(USART1, USART_FLAG_TC)
,所以在上面串口初始化的函数里使能串口后加入这个函数,就能解决问题②了。
发送一个字节的数据:
这里因为只发送一个字节的数据,所以检测TXE的标志位就可以了,当然检测TC标志位也可以。
//发送一个字节的数据
void UART_SendByte(uint8_t dat)
{
USART_SendData(USART1,dat);
while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET);
}
发送二个字节的数据:
因为这里也是一个字节一个字节的发送数据,所以检测TXE标志位即可。
//发送二个字节的数据
void UART_SendHalfWord(uint16_t dat)
{
uint8_t dat_h = 0;
uint8_t dat_l = 0;
dat_h = (dat & 0xFF00) >> 8;
dat_l = dat & 0XFF;
USART_SendData(USART1,dat_h);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
USART_SendData(USART1,dat_l);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}
发送一个数组的数据:这里很重要,我出现了两个问题。
//发送一个数组的数据
void UART_SendArray(uint8_t *array,uint8_t num)
{
uint8_t i = 0;
for(i = 0; i < num; i++)
{
USART_SendData(USART1,array[i]);
while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET);
}
while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET);
}
关于与TXE和TC相关的一套流程:
对于USART的数据发送有两个标志,一个是TXE(发送数据寄存器空),另一个是TC(移位寄存器发送结束);当TDR中的数据传送到移位寄存器之后,TXE被置位为1,此时移位寄存器开始想TX信号线按位传输数据,但因为TDR现在是变为空的,程序可以把下一个要发送的字节通过操作USART_DR寄存器写入TDR中,而不必等到移位寄存器中所有位发送结束,所有位发送结束时(送出停止位后),硬件为设置TC标志为1.
另一方面,在刚刚初始化好USART还没有发送数据时,也会有TXE标志,因为这时发送数据寄存器是空的。这也是为什么TXE 和 TC在复位状态时是1了。
那么对于我上面出现的问题就很好解释了,首先在串口使能之后,CPU会发出一个空闲帧,理论上它是0XFF,一个字节的数据作为空闲帧,因为此时移位寄存器是空闲的,所以空闲帧数据直接被发送到移位寄存器,TXE此时置位,然后空闲帧正在按位向外发送,而我出现的第一个问题就是由于没有等待移位寄存器的数据发送完成就开始给TDR寄存器写入下一个数据,导致数据被破坏,只有最后一个数据是完整的等待其发送完成的,所以最后串口输出的是0X0A,所以我在每一个字节的数据写个USART_DR寄存器后,都检测TC是否被置为1,当TC被置为1,说明上一帧的数据以及从移位寄存器发送完成,此时TDR的数据被传送到移位寄存器了,才能进行下一个TDR寄存器的写入。
对于第二个问题就是,因为TC在处理器复位之后默认是置1的,且串口使能后默认会发出一个空闲帧,发送完毕之后TC也是置1的(值得注意的是这里还引起一个问题就是串口初始化之后马上就进入了TC中断,所以为了避免这种情况,可以在串口使能后等待空闲帧发送完毕,再打开TC中断),因为发空闲帧的时候前面的移位寄存器属于空闲状态,所以空闲帧是直接到移位寄存器,且TXE被置1,然后空闲帧正在一位一位往前传的过程中,我们就已经进入了循环里面发送一个字节的函数,他会把数组第一个元素写到TDR寄存器里,但是由于当前TC是被置1的,因为没有被清零(只有先读SR寄存器,再写入DR寄存器才能给TC清零,当前是先写的DR寄存器,才读的SR寄存器,所以TC未被清零),所以第一个while循环就被跳过了,此时空白帧还没发送完,但是TDR里存放的第一个元素的数据被第二个元素给覆盖了。然后由于读了SR寄存器才写的DR寄存器,此时TC寄存器是被清零了,所以后面的数据是完完整整发送成功的。
关于串口scanf函数重定向解决办法
scanf函数重定向用到fgetc函数,检测USART_IT_RXNE标志位,同时串口中断函数USART1_IRQHandler里也要检测这个标志位,二者冲突,所以scanf函数不好使,串口初始化时不使能中断scanf函数就好使了。
建议:在调试过程中可以Disable串口中断,然后就可以使用用scanf函数,实际使用时如果使能了中断,就不能用scanf函数。
重定向fgetc函数:
int fgetc(FILE *f)
{
while((USART1->SR & USART_IT_RXNE) == RESET);
//while((USART1->SR & 0X20) == RESET); 两个while函数实现的功能相同,哪个顺眼用哪个
return (int)(USART1->DR);
//return(int)USART_ReceiveData(USART1); 两个return功能返回值相同,哪个顺眼用哪个
}
因为使用的是正点原子提供的程序,没有勾选微库,此时遇到第一个问题 Error: L6200E: Symbol __stdout multiply defined (by stdio_streams.o and usart.o). 问题是stdout重定义,原因是没有勾选use MircoLIB;
然而勾选了微库之后又出现一个问题:勾选以后遇到第二个问题Error: L6915E: Library reports error: __use_no_semihosting was requested, but a semihosting fgetc was linked in
原因是原子的一段代码,可以不选择微库(use MircoLIB),既然选择了微库,那就矛盾了,所以我更改代码如下:简单来说就是不用原子这段代码,选择微库。
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#if 0
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//FILE __stdin;
//定义_sys_exit()以避免使用半主机模式
_sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
//return (SendChar(ch));
}
int fgetc(FILE *f)
{
while((USART1->SR & USART_FLAG_RXNE) == RESET);
return (int)(USART1->DR);
}
#endif
#if 1
//重定向printf函数
int fputc(int ch, FILE *f)
{
//USART_SendData(USART1, (uint8_t) ch);
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
int fgetc(FILE *f)
{
while((USART1->SR & USART_IT_RXNE) == RESET);
//while((USART1->SR & 0X20) == RESET); 两个while函数实现的功能相同,哪个顺眼用哪个
return (int)(USART1->DR);
//return(int)USART_ReceiveData(USART1); 两个return功能返回值相同,哪个顺眼用哪个
}
#endif
如果坚持要使用原子的方式,不使用微库,那么可以在原子的代码上加上一句:
FILE __stdin;
加入上面一句代码后从而达到不用微库,重定向printf和scanf函数的目的。
到此,可以用scanf函数来读取了,但是出现新的问题:数据读取不全,且随机变化,且多次输入才能显示一次数据。
比如,我想输入12345空格,但是串口读取的可能只有34,下一次是52,再下一次是1435132。。。
最终发现:
fgetc函数中检测USART_IT_RXNE标志位,同时串口中断函数USART1_IRQHandler里也要检测这个标志位,二者冲突。
将串口中断函数Disable后就可以正常使用scnaf函数了。