完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
一、通信方式相关
1.1 并行通信 1.2 串行通信 串行通信的通信方式: 常见的串行通信接口: STM32的串口通信接口
三、UART异步通信方式特点 四、串行通信的过程
串口初始化结构体 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;//接收数据错误,重新开始接收 } } } } } 首先定义了下面几个变量:
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 单字节通信
十二、串口通信实验 不使用中断之前:串口的初始化函数 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); } 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); 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); 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; 1 参考帖子链接点击这里 加入上面一句代码后从而达到不用微库,重定向printf和scanf函数的目的。 到此,可以用scanf函数来读取了,但是出现新的问题:数据读取不全,且随机变化,且多次输入才能显示一次数据。 比如,我想输入12345空格,但是串口读取的可能只有34,下一次是52,再下一次是1435132。。。 最终发现: fgetc函数中检测USART_IT_RXNE标志位,同时串口中断函数USART1_IRQHandler里也要检测这个标志位,二者冲突。 将串口中断函数Disable后就可以正常使用scnaf函数了。 |
|
|
|
只有小组成员才能发言,加入小组>>
调试STM32H750的FMC总线读写PSRAM遇到的问题求解?
1771 浏览 1 评论
X-NUCLEO-IHM08M1板文档中输出电流为15Arms,15Arms是怎么得出来的呢?
1619 浏览 1 评论
1070 浏览 2 评论
STM32F030F4 HSI时钟温度测试过不去是怎么回事?
724 浏览 2 评论
ST25R3916能否对ISO15693的标签芯片进行分区域写密码?
1673 浏览 2 评论
1936浏览 9评论
STM32仿真器是选择ST-LINK还是选择J-LINK?各有什么优势啊?
729浏览 4评论
STM32F0_TIM2输出pwm2后OLED变暗或者系统重启是怎么回事?
569浏览 3评论
594浏览 3评论
stm32cubemx生成mdk-arm v4项目文件无法打开是什么原因导致的?
552浏览 3评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-12-22 21:17 , Processed in 0.509800 second(s), Total 44, Slave 37 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号