串口驱动不简单,在实际工作中,往往串口驱动框架的设计都是需要考虑的非常清楚的,特别是实际的项目中。比如基于串口模块功能的协议开发,以及基于串口模块的网络数据收发等等,都是一些值得好好设计和思考的问题。本文目的是总结一下目前我见到过的常用的几种模型,并且对这些设计提出自己的一些想法。
简单的串口的使用就是收发数据,当串口数据到来后,通过中断通知,拷贝到一个内存固定数组中,下次协议需要处理的时候,直接从该数组中去取数据。
这种设计的思维逻辑是比较理想化的模型,这种模型的思考主要是主从设备的考虑,按照下面的逻辑来进行:
1.主机发送数据到模块
2.等待一定时间后,模块回复数据给主机
3.此时MCU会通过串口中断接收到一帧数据,然后拷贝到固定的数组中
4.MCU去处理固定数组中的数据,然后清空数组
以上的设计从原理上没问题,似乎没有任何漏洞。但是实际上,真实设备的通信往往不会那么完美,会出现以下的情况:
1.模块发过来的数据,可能有断帧的情况,明明模块只需要一帧回复就可以,但是因为一些原因导致程序分为前后两帧数据发过来,这样导致第二帧覆盖了数据,导致数据不准确。
2.设备的外部电平干扰,这种情况发生在主机给从机发送完成后,等待从机回复后,缓冲区中的数据没有及时取走,外部信号导致主机认为一帧数据的到来,从而破坏了缓冲区中的数据。
3.主机发送数据后,等待从机回复数据的时间不确定,有可能导致处理数据时还在接收数据。
这三种风险的存在,使得往往真实的设备上出现了许许多多问题,导致设备的使用体验不佳,或者程序出现频频的bug。为了解决这些问题,不同的嵌入式程序员在上面思考了很多解决办法。
这种设计就是因为看到了模块的数据发送的特点,以及数据没及时取走就被覆盖的问题,于是设计了一个带有接收数量的缓冲区模型。
前面一种是基于串口的帧数据模型,而缓冲区考虑的是串口的字节模型。帧数据模型一般就是串口接收的时候,发生串口接收中断,把数据放到缓冲区,当一帧数据接收完成后会发生空闲中断,或者DMA完成中断,或者是采用定时器时定时器中断,这样判定接收到一帧数据。而循环缓冲区则不需要考虑一帧数据完成的中断,这种设计都是在软件中完成。
这种设计都会采用一个USART_RX_STA
接收状态寄存器的变量来表明接收数据的状态,通用的设计规则是
1.USART_RX_STA
的0~14位用于存储串口接收数据的个数,每次接收完成都会将USART_RX_STA++
。
2.USART_RX_STA
的15,16两位表示接收的1状态,简而言之,比如接收状态0x4000表示接收到了0x0d,而0x8000则表示接收完成。
数据处理在任务中,当判断数据接收完成时,处理缓冲区中的数据即可。这种设计很多开发板例程有,所以很多人直接拿开发板例程做项目时经常可以看到。
问题也是很明确,当做协议处理,十分不友好,需要及时去取数据,否则会掉帧。当数据接收的很快时,也会出现问题。
这种我只在轨道交通某些程序上看到这种设计,设计是基于报文格式,这种报文处理方式的好处很明显,可以一个字节一个字节的处理,比如可以判断报头是0x10
,第二个报文是0x02
等等,然后依次往数据包中处理写数据,判断结尾帧后,一次性交给其他程序处理数据包。
这个数据包的处理放在串口接收中断里面,然后去解析报文,获取相关的信息。
基于报文的解析规则,需要的串口波特率比较低,比如9600以下等等,并且采用485等进行数据传输。这样可靠性有一定的保障,但是数据的发送一定需要在比较低的波特率下进行。而且两帧数据之间需要有一定时间的间隔。
这一种设计需要一个read_index指针和write_index指针。
设计的关键点在于读写分离,每次去读数据的时候,不用关注是否一帧数据,直接从循环buffer中去取数据就可以了。当read_index=write_index
的时候,表示buffer中的数据已经处理干净了。
这种读写分离的设计考虑可以参见rt-thread物联网操作系统的serial框架设计。
这种设计从一定程度上,避免了数据丢包的情况,但是在读数据的时候,如果出现了连包的情况,处理起来也比较的复杂。特别是在不定长数据协议栈的处理上,也会显的无力。
对于收发有序的逻辑处理比较好,但是不定期发送数据,处理起来也需要一定工作量。
目前,正在做一个基于物联网的通用系统模型,所以思考了一下基于物联网上面的串口模块使用的架构,最后设计了一个可以使用的框架。当前不一定很完善,但是还是把自己思考的部分分享出来。
由于物联网模块的AT指令都是不定长的,而且时一帧一帧的数据,而且实际透传模式时,收发没有固定的逻辑,都是随机的,所以针对这种特性设计以下的模型。
其设计思想基于串口中间件的考虑,上层应用不直接访问串口驱动硬件。每次都调用同样的接口去包管理器中去取一帧数据,所以这种设计的出发点是基于一帧数据的模型。关于确保一帧数据的机制,可以使用定时器去判断,如果在一定的时间里面没有收到数据,发生了定时器中断,那么就认为这一帧数据的结束。如果这个假设出现了太多问题,那么这个设计也是不行的。
而通用接口又不用关注是否有数据到来的问题,直接调用接口,有数据则返回数据信息,没有数据则告诉没有数据。
所有的数据包管理在一个对象结构体中进行,读数据时,将data_r_index
向后偏移,比如最大定义8个pkgs,那么达到8时再回到0,然后去读对应index的数据。并且读数据,会有rest_size
,表示这个里面还剩下多少数据没有读完,下次调用读函数时,直接去处理没读完的数据就好了,而不用去找到第二帧数据。
int uart_buffer_read(char *uart_buf, uint16_t len)
{
int ret = 0;
//两者相等,无数据
if(uart_manage.data_w_index == uart_manage.data_r_index)
{
ret = 0;
}
else
{
if(len >= uart_pkg[uart_manage.data_r_index].rest_size)
{
len = uart_pkg[uart_manage.data_r_index].rest_size;
rt_memcpy(uart_buf, uart_pkg[uart_manage.data_r_index].user_data + (uart_pkg[uart_manage.data_r_index].recv_size - uart_pkg[uart_manage.data_r_index].rest_size), len);
uart_pkg[uart_manage.data_r_index].rest_size = 0;
ret = len;
uart_pkg[uart_manage.data_r_index].recv_size = 0;
if(uart_manage.data_r_index < 7)
{
uart_manage.data_r_index = uart_manage.data_r_index + 1;
}
else
{
uart_manage.data_r_index = 0;
}
}
else
{
rt_memcpy(uart_buf, uart_pkg[uart_manage.data_r_index].user_data + (uart_pkg[uart_manage.data_r_index].recv_size - uart_pkg[uart_manage.data_r_index].rest_size), len);
uart_pkg[uart_manage.data_r_index].rest_size = uart_pkg[uart_manage.data_r_index].rest_size - len;
ret = len;
}
}
return ret;
}
对于串口写数据的设计,则是由接收到一帧数据后触发,由中断触发,此时一帧数据接收完成,会有一定的休息时间,所以一般足够将收到的数据写到对应的包中。
int uart_buffer_write(char *uart_buf, uint16_t len)
{
if(len > 512)
{
rt_kprintf("write buffer too big!\n");
return 0;
}
rt_memset(uart_pkg[uart_manage.data_w_index].user_data, 0, 512);
uart_pkg[uart_manage.data_w_index].recv_size = len;
uart_pkg[uart_manage.data_w_index].rest_size = len;
rt_memcpy(uart_pkg[uart_manage.data_w_index].user_data, uart_buf, len);
uart_manage.buff = &uart_pkg[uart_manage.data_w_index];
if(uart_manage.data_w_index < 7)
{
uart_manage.data_w_index = uart_manage.data_w_index + 1;
}
else
{
uart_manage.data_w_index = 0;
}
return len;
}
通过上述的设计逻辑,很好的处理了一帧数据包的问题,同时每帧数据都带有长度信息和相关的读写状态信息,也能够不丢帧。同时采用循环index的机制,能够不干扰数据正常的存取的情况下,保障的数据的可靠性。
当然,这种设计的前提是保证一帧数据的完整性与可靠性的比例很高,如果断帧的情况比较严重,那还是采用循环buffer,通过字节管理的方式进行设计。
对于串口框架的设计,是需要好好思考的,设计串口驱动程序时,不要认为串口驱动简单,在做协议时,也不能太过于数据传输的理想化,应该综合考虑连包、断帧、超时、干扰等等因素,这样设计的驱动才会更加的稳定,上文只是简单的描述常见的一些处理串口数据的逻辑,应该还有更多的,更好的设计框架值得去学习理解,也希望有更多的人提出一些更好的设计模型。
1.2021年第3期《单片机与嵌入式系统应用》电子刊新鲜出炉!
2.MCU为什么内部不集成晶振?
3.MCU开发中,你选”裸奔“还是RTOS?
4.MIT发布2021年10大突破性技术~
5.Google重磅发布Flutter 2!一套代码横扫 5 大系统
6.硬件工程师常用的5V转3.3V的方法
免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。