自我介绍
为什么要写个自我介绍?俗话说,光说不练假把式,光练不说傻把式, 因为本人一直很自负,感觉什么东西都可以很快上手(当然非对口专业知识要求高的技能除外),如果我不把这自负表现出来,感觉会憋出内伤。
本人于2015年毕业于电子科技大学,开始的理想是进入一家国外的游戏公司,但理想很丰满,现实很骨感,给育碧、2k Game、Gameloft、EA都投了简历,但都被无情pass掉了(在此吐槽一下,这些公司真的是想在校园里招本科生吗?明明是研究生的计算机图形学却成了这些公司对本科生的要求)。后来毕业了也没找到工作,为了自力更生,就随便去了家电气公司,也就是我现在的公司。
这是家才创建的公司,业务是做智能应急疏散照明系统这一块,虽然老总也给我说以后要搞物联网,但我感觉这是当初忽悠我来公司的。公司对软件开发的认识基本停留在零,也不愿意花钱请个高级软件工程师,因此我成了公司的独苗,既要负责高级程序的开发,又要进行底层开发。虽然在学校里没用过C#,但语言都有相通之处,因此对于现在公司的上位机程序的开发还是游刃有余,但底层开发的经验基本没用。虽然之前在一家公司的试用期中,我开始接触了单片机,但因为我本人不喜欢纯拷贝别人的代码,所以当公司要我给51单片机写个串口通信的时候,浏览了下STC的数据手册就模棱两可的进行开发,然后卡住了,对比了网上的那些程序,感觉自己的步骤没有错啊,这次事件让自诩学习能力强的我备受打击,于是从那家公司辞职了,现在回想起来,可能当时我没注意到51单片机的串口的波特率发生器只能使用T1,而我一直使用T0作为波特率发生器。
最近开始接触STM32系列芯片,因为有固件库对底层寄存器的操作进行了封装,很接近高级编程,学起来跟用起来都很方便,而且现在很多移动芯片也是基于ARM架构的,所以学好ARM芯片也是敲开很多大型企业的敲门砖(我就想用它敲开华为的大门)。
问题描述
网上搜索“STM32 BUG”,就会搜到困扰很多STM32开发者的的两个问题,一个是关于I2C的,另一个就是关于串口发送数据丢失第一帧的问题。因为我还接触I2C,所以第一个问题是怎么回事我不知道。虽然第二个问题,网上也有问题原因的解答,而且按照这些答案中的方式来编写串口发送程序可以防止丢失第一帧数据。因为我不想照搬别人的程序,因此在机缘巧合中,让我发现了对这个问题的另一种解释,而且这种解释推翻了之前网友们提供的解释。
问题分析
以下是官方的STM32的参考手册对串口发送数据的过程描述:
- 通过在USART_CR1寄存器上置位UE位来激活USART
- 编程USART_CR1的M位来定义字长。
- 在USART_CR2中编程停止位的位数。
- 如果采用多缓冲器通信,配置USART_CR3中的DMA使能位(DMAT)。按多缓冲器通信中的描述配置DMA寄存器。
- 设置USART_CR1中的TE位,发送一个空闲帧作为第一次数据发送。
- 利用USART_BRR寄存器选择要求的波特率。
- 把要发送的数据写进USART_DR寄存器(此动作清除TXE位)。在只有一个缓冲器的情况下,对每个待发送的数据重复步骤7。
对于导致串口丢失数据的步骤我同意网友的观点,那就是步骤5中,在置位TE的时候,发送了一个空闲帧作为第一次数据发送。但对于为何发送空闲帧会导致用户数据丢失第一帧的原因,我和网友的看法不一致。
网友的观点可以概括为两个字,覆盖。这是网友的普遍看法,http://blog.csdn.net/kevinhg/article/details/40991655,看到一些网友引用了这篇博客,而且很多网友的观点基本跟这篇博客中的一致。从这篇博客中可以看出,网友觉得是在发送第一个数据的时候,没有读USART_SR,所以TC仍然处于置位,在写入第二个数据的时候,就覆盖掉了第一个数据。
于是我就按照这篇博客中的解释,写了个发送程序,关键代码如下:
/* 发送数据 */ if(USART_GetFlagStatus(USART3,USART_FLAG_TC)!=RESET) { if(TxCount3<buf3_len) { USART_SendData(USART3,rx3_buf[TxCount3]); TxCount1=USART_GetFlagStatus(USART3,USART_FLAG_TC); TxCount3++; } else { USART_ITConfig(USART3,USART_IT_TXE,DISABLE); //发送完成,关闭TXE,避免TDR空中断 } }
if语句中我读取USART_SR的状态,然后发送第一个数据,根据官方参考手册的说明,此时TC被清零,那么就不存在第二个数据覆盖第一个数据的情况发生,但实际上,第一帧数据仍然丢失了。网友可能会说,按照这种方式,向USART_DR中写入第一个数据并没有让TC清零,那么以下是我修改了的程序,主要监视向USART_DR中写入第一个数据后,TC是否被清理:
if(USART_GetFlagStatus(USART3,USART_FLAG_TC)!=RESET) { if(TxCount3<buf3_len) { USART_SendData(USART3,rx3_buf[TxCount3]); TxCount1=USART_GetFlagStatus(USART3,USART_FLAG_TC); TxCount3++; } else<span style="color:#ff6666;"> </span> { USART_ITConfig(USART3,USART_IT_TXE,DISABLE); //发送完成,关闭TXE,避免TDR空中断 } }
调试截图如下:
从监视窗口中可以看到,TxCount3=0,表示我发送的是第一个数据,而向USART_DR中写入了第一个数据后,TxCount1=0,说明TC被清零,跟官方参考手册说法一致,那么就不应该出现第二个数据覆盖第一个数据的啊。
因为缺少对硬件的具体实现的了解,我的观点只是一种猜测。在置位TE后,TC与TXE都是被置位的(这解释了为何置位TE后,并使能TXE,TC会不断进入中断程序),并且发送一帧空闲帧作为第一个数据,根据官方参考手册,TC置位表示发送移位寄存器已空,可以向USART_DR中写入数据,照理说此时可以写入用户的第一个数据,但真实情况是发送移位寄存器还装有空闲帧的数据,即空闲帧并未发送完成(这解释了为何置位TE后,延时一段时间再发送数据就不会丢失第一个数据,因为这段延时期间空闲帧发送完成了),在空闲帧还没发送完成的时候,推测官方提供了对移位寄存器的硬件保护,即即使TC置位,此时USART_DR中的数据也不会装入发送移位寄存器,而是被忽略掉了。推测发送移位寄存器的硬件保护条件:移位寄存器为发送完成,TC已经被置位,虽然想写个程序验证下,但TC只能由硬件置位。
第一个数据被写进了TDR中:
/* 发送数据 */ if(USART_GetFlagStatus(USART3,USART_FLAG_TXE)!=RESET) { if(TxCount3<buf3_len) { USART_SendData(USART3,rx3_buf[TxCount3]); TxCount1=USART_GetFlagStatus(USART3,USART_FLAG_TXE); TxCount3++; } else { USART_ITConfig(USART3,USART_IT_TXE,DISABLE); //发送完成,关闭TXE,避免TDR空中断 } }
进入了if 的内部,说明之前TXE被置位,而写入第一个数据后,TxCount1=0,即TXE被清零,说明第一个数据写入了TDR中。
因此在之前推测基础上进一步补充串口的细节。当发送移位寄存器发送完成后,会先读取TC的状态,若TC已被置位,则串口忽略TDR中的数据(这也符合官方参考文档的描述,当TC置位时,表示发送移位寄存器为空,向USART_DR中写入数据会直接放进发送移位寄存器,而存放了第一个数据的TDR被忽略掉了,这也解释了第一个数据丢失的问题),若TC未被置位,检测TXE是否置位,若TXE置位,则置位TC,若TXE复位,则从TDR中载入数据到发送移位寄存器,载入结束后,置位TXE。
所以,其实这并不是什么STM32的串口发送bug,而是STM32提供了对发送移位寄存器的非空保护(该非空并不是说非零,记录移位的计数器非零),而其执行过程都是按照了官方参考文档的描述。非要说是bug的话,就是STM32置位TE后,在发送空闲帧的过程中,不应将TC置位。
总结
当发送移位寄存器发送完成后,会先读取TC的状态,若TC已被置位,则串口忽略TDR中的数据(这也符合官方参考文档的描述,当TC置位时,表示发送移位寄存器为空,向USART_DR中写入数据会直接放进发送移位寄存器,而存放了第一个数据的TDR被忽略掉了,这也解释了第一个数据丢失的问题),若TC未被置位,检测TXE是否置位,若TXE置位,则置位TC,若TXE复位,则从TDR中载入数据到发送移位寄存器,载入结束后,置位TXE。