串口在我印象中是从来不会丢包的,那是因为以前都是用的厂家提供的SDK,现在用MCU裸板开发,自己做驱动,如果驱动没做好,就会丢包。
今天来总结两个串口驱动层的丢包问题,一个是发数据丢包,即实际发出的数据比预期发的少;一个是收数据丢包,即实际收到的数据比对端发出的数据少。
1、发数据丢包
调试过程发现,当应用层连续两次调用驱动层的串口发数据接口去发数据时,对端wifi板收到的数据会比发出的少了1byte!这么诡异的问题,是不是wifi板驱动层有问题?因为毕竟自己写的代码总觉得是那么完美哈哈哈,然而wifi板是厂家提供的SDK,经过市场验证很久了,大概率不会有bug。
先来了解一下MCU这边串口发数据的过程,用的是51内核的某款芯片。
用户要用串口发数据时,把一个发送数据寄存器置1(即设置TI =1),然后底层会自动产生发数据的中断,
在发数据中断服务函数中,用户要做的是:
1、 把TI 清零,即TI =0;
2、 把要发的数据装载到数据寄存器(SBUF),每次只能装载1个byte。
串口的发送器就开始发送数据,当1byte数据发送完成时,硬件会自动把TI置1,再次产生写数据中断,在写数据中断里用户继续重复刚刚的操作。
那么数据发送是怎么结束的?当某次进入发数据中断,用户把TI清零,没有再往数据寄存器装载数据时,就不会再产生发数据中断,发送过程就结束了。
原来的代码大致如下,
//驱动层用于发数据的先进先出的环形fifo,大小256
char *send_buf_fifo[256];
//fifo写指针,指示fifo数据装到什么地方了,如果装到末尾了,又会返回头部从头部装
int tx_index;
//fifo读指针,指示当前数据发送发到哪了
int rx_index;
//驱动层发送数据接口实现
int uart_send(char *buf, int len)
{
//buf 是要发的数据缓存指针,len是要发的长度
把buf中数据拷贝到驱动层发数据的 fifo中,代码略
tx_index += len; // Fifo 写指针 往后移len;
tx_index = tx_index%256;
TI =1;//启动发送
}
//中断服务函数
void uart_isr()
{
if(TI )
{
TI = 0;
//fifo中数据还没有发完
if(tx_index != rx_index)
{
//把fifo中没发的数据中的第1个byte装载到串口数据寄存器;
SBUF= send_buf_fifo[rx_index];
rx_index++;
rx_index = rx_index %256; //Fifo读指针往后移;
}
}
调试发现,当应用层连续两次调用uart_send接口时,会发生丢包现象,wifi板收到的数据少了1 byte,如果应用层调用uart_send接口是每调用一次隔开一段时间再就不会出现丢包现象。
那么连续调用时,丢包是怎么发生的?连续两次调用uart_send函数,当第二次调用时,第一次调用uart_send函数发的数据驱动层应该还没有发完,应该还在一次次产生中断发数据。
此时应用层又调用uart_send函数,把数据装载到fifo中,这没问题,问题在于应用层把 TI = 1了。如果此时串口的硬件发送器正在发数据,此时应用层把TI置为1了,就会自动马上进入串口发数据中断中,刚刚正在发的那1byte数据还没有成功发出去,此时也就丢掉了。
正确的做法应该怎么样?uart_send函数中把 TI置为1之前先判断一下,此时驱动层是不是正在发数据过程中,即fifo中有原来的数据还没有发完,如果是,那么不用再次把TI置为1;
修改如下:
char sending_flag = 0; //发送标志位,指示当前是否正在发送数据过程中
//驱动层发送数据接口实现
int uart_send(char *buf, int len)
{
//buf 是要发的数据缓存指针,len是要发的长度
把buf中数据拷贝到驱动层发数据的 fifo中,代码略
tx_index += len; // Fifo 写指针 往后移len;
tx_index = tx_index%256;
//若没有在发送数据去启动发送,若正在发送中不用再次启动
if (sending_flag == 0)
{
TI =1;//启动发送
sending_flag = 1;
}
}
//中断服务函数
void uart_isr()
{
if(TI )
{
TI = 0;
//fifo中数据还没有发完
if(tx_index != rx_index)
{
//把fifo中没发的数据中的第1个byte装载到串口数据寄存器;
SBUF= send_buf_fifo[rx_index];
rx_index++;
rx_index = rx_index %256; //Fifo读指针往后移;
}
else //若fifo中数据已经发送完,清发送指示标志
{
sending_flag = 0;
}
}
这样修改之后连续两次调用发送接口,就不再有丢包的问题出现了。
2、收数据丢包
把各个模块代码全部跑起来调试发现,用电脑模拟wifi板给MCU发送指令时,大概率出现了丢包现象,MCU收到的数据包不完整,比实际电脑发出的少,偶尔才能收到一包完整的。
电脑串口驱动层总不会有问题吧?哈哈哈,这时候不怀疑自己程序有问题都不行了。
驱动层打印log发现,驱动层收到的数据确实少了,收数据是在串口接收中断中收的,在中断服务函数中用户把数据寄存器的数据拷到自己的缓存中去,初步怀疑是串口接收中断丢了。
之前有一个版本是好的,对比那个版本代码发现,有问题的版本LED驱动用的是可调光模式,即可以调亮度的模式,MCU包括一个LED驱动,有8个COM口16个SEG口,最多可以接8*16=128个LED灯。可调光模式与不可调光模式的区别就是,在LED中断服务服务函数里,不可调光模式是不做任何事直接返回,可调光模式要去执行一段代码,去给当前COM周期下的灯进行亮度赋值。
那是不是串口收数据中断被led中断耽误了?
查芯片手册了解到,
1、这款芯片中断是有优先级的而且可以嵌套,低优先级中断可以被高优先级中断打断,
2、如果各个中断优先级相同,当同时发生中断时,就看轮询优先级,即按一定顺序轮询,先查到谁就去执行谁的中断服务函数。
3、如果不去设置优先级,那各个中断优先级是默认都是最低优先级。
那么当串口有数据要收时中断多久发生一次?数据是1byte 1 byte收的,也就是1byte发生一次中断,波特率115200,也就是115200 bit 每秒,那么发送1 byte 用的时间是 1/115200 *9 = 78.12us(这里乘以9是因为串口设置的是8个数据位1个停止位,也就是发送1byte数据实际发送有8位数据+1位停止位);
系统时钟用的是24M,一条空指令nop时间是 1/24M =0.042us,平均一条指令算5个nop,0.042 *5= 0.21us;
那78.12us之内大概可以执行多少条指令?78.12/0.21 = 372;
也就是说如果在LED中断服务函数里的指令超过了大概372条汇编指令,而且LED中断刚发生马上又发生了串口中断,此时在LED中断服务函数里面呆的时间太长了,等从LED中断出来时,串口又再次产生了新的中断,把老的中断覆盖了,也就是说此时串口SBUF寄存器里装的就是新的数据了,把老的数据覆盖了,刚刚收到的老数据漏掉了,没有传给用户的缓存。
如何解决?
设置串口中断优先级为最高,如果在led中断服务函数里面时,串口接收中断产生了,由于串口中断优先级>led中断优先级,那么CPU会马上从LED中断出来去执行串口接收中断服务函数,如果LED中断和串口中断同时产生,会先执行串口中断函数;
这样就保证了串口数据接收不会丢包。
LED中断被影响关系不大,顶多某次闪灯或者亮灯不太对(目前没发现不对),但是串口数据丢了就是个大问题,会出现控制设备不成功,影响用户体验。
把串口中断设为最高优先级之后丢包问题解决。
关于串口收数据丢包问题之前也出现过,把系统时钟调低到6M就会出现115200波特率收数据丢包,调高为12M、24M就好了,当系统时钟调到6M时,意味着执行一条指令的时间变长了,应该也是串口中断被自己的或者其他的中断服务函数耽误了。
算了下,时钟6M时,一个nop是1/6M =0.167us,在串口的一个中断周期内,中断服务函数如果执行大于93条汇编指令就可能耽误串口中断,此问题后续再验证看一下。
免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。
来源:oschina
链接:https://my.oschina.net/u/4274857/blog/4770703