最近项目需求,要用到RK3568搭配自制底板。整个软硬件联调过程并不顺利,特立此系列帖,记录调试中发生的一些问题和解决办法。
文章目录
前言
由于硬件在选485芯片的时候,没有选择自动控制收发io的芯片,也没有在电路上进行优化,导致在使用自制板的RS485通讯时,需要在应用层控制485收发方向,但在应用层控制可能会导致切换不及时而造成数据丢包。下面会介绍其中问题细节和我的解决办法。
调试过程及问题
自制板在RS485部分,使用的芯片是TDH301D485H-E,UART_RE为收发方向IO,RX,TX即收发引脚。部分电路图如下所示:
相比于普通的UART,在485的使用上多出了一个收发IO口。Linux提供了相关的控制接口,最初的想法是在串口发送数据之前加上一个GPIO的电平控制。默认电平为高(485读状态),在发送前将电平拉低,进入发送状态,发送后再将电平拉高进入读状态。
GPIO导出控制相关函数:
int set_gpio_export(unsigned int gpio)
{
int fd, len;
char buf[128];
fd = open( "/sys/class/gpio/export", O_WRONLY);
if (fd < 0)
{
perror("gpio/export");
return -1;
}
len = snprintf(buf, sizeof(buf), "%d", gpio);//从数字变换为字符串,即1 变为”1“
write(fd, buf, len);//将需要导出的GPIO引脚编号进行写入
close(fd);
return 0;
}
int set_gpio_direction(unsigned int gpio, unsigned int io_flag)
{
int fd, len;
char buf[128];
len = snprintf(buf, sizeof(buf), "/sys/class/gpio" "/gpio%d/direction", gpio);
fd = open(buf, O_WRONLY);
if (fd < 0)
{
perror(buf);
return -1;
}
if (io_flag)//为1,则写入“out",即设置为输出
write(fd, "out", 4);
else//为0,则写入“in",即设置为输入
write(fd, "in", 3);
close(fd);
return 0;
}
int set_gpio_value(unsigned int gpio, unsigned int value)
{
int fd, len;
char buf[128];
len = snprintf(buf, sizeof(buf), "/sys/class/gpio" "/gpio%d/value", gpio);
fd = open(buf, O_WRONLY);
if (fd < 0)
{
perror(buf);
return -1;
}
if (value)//为1,则写入“1",即设置为输出高电平
write(fd, "1", 2);
else//为0,则写入“0",即设置为输出低电平
write(fd, "0", 2);
close(fd);
return 0;
}
发送485时拉高,发送完立即拉低,示例:
set_gpio_export(gpio_num);
set_gpio_direction(gpio_num, GPIO_OUT_MODE);
set_gpio_value(gpio_num, RS485_SEND_MODE);
write(fd, data, sizeof(data));
set_gpio_value(gpio_num, RS485_RECV_MODE);
但是经测试使用此方式发送完后,接收方收不到数据或者只能收到几个字节的数据,原因在于应用层调用write函数后,数据会交给内核处理,假设115200波特率下发送一包256个字节的数据,理论上的发送时间是256101000/115200大约22ms左右,而应用层write函数调用时间是us级,在调用完后如果立即切换引脚状态,缓冲区中的数据还没有发送完,就会导致数据丢失。
在此现象下,想到了一个函数tcdrain()。既然需要等到缓冲区发送完毕后才能切换状态,那就在调用完write后,检查缓冲区,直到为空时再去切换。
发送485时拉高,发送完等待缓冲区数据发送完毕再拉低,示例:
set_gpio_export(gpio_num);
set_gpio_direction(gpio_num, GPIO_OUT_MODE);
set_gpio_value(gpio_num, RS485_SEND_MODE);
write(fd, data, sizeof(data));
tcdrain(fd);
set_gpio_value(gpio_num, RS485_RECV_MODE);
经过优化后,再进行测试,只是单测收发确实发送的数据能被接收到了。
但在模拟485的使用场景(连接板卡的485_1和485_2,485_1发送数据,后等待回复,485_2等待接收,接收到数据后立即返回数据。)测试时,又发现1能成功发送并且2能接收到,但是2返回的数据已经丢了。
经调试发现,tcdrain这个函数,实际阻塞的时间要远大于理论发送时间,在上述波特率和数据包大小情况下,tcdrain阻塞最高等到32ms,而在发送方阻塞时,接收方已经接收到了数据并将数据返回了,这才导致了数据丢失。如此在应用层来控制收发在实际使用上还是有很大可能会造成丢包,在项目上是不允许的,下面说一下解决办法。
解决办法
1.硬件修改
其实硬件上修改为自动收发控制电路或使用自动收发控制的芯片是最好的解决办法。例如更换为TDH301D485H-A型号
当然,芯片更换如果封装不同可能会很麻烦,成本也可能会增加。也可以通过优化电路来实现,例如正点原子的参考电路:
通过增加一个三极管电路,实现自动切换方向,详细的电路原理这里不再展开。
2.软件解决
除了修改硬件,在软件上,我们也可以通过修改linux的内核,在内核侧来控制IO口,会大大的缩短延时,从而达到避免切换延时带来的数据丢包问题。下面以我使用的RK3568作为参考,介绍一下修改步骤。
1.修改设备树文件
在设备书中串口节点中,增加485_ctrl_gpio信息,gpio根据实际使用的io号修改
&uart3 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart3m1_xfer>;
485_ctrl_gpio = <&gpio1, RK_PB2, GPIO_ACTIVE_HIGH>;
};
2.查找设备树对应的串口驱动文件
在rk3568.dts中可以找到串口相关的节点:
uart1: serial@fe650000 {
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
reg = <0x0 0xfe650000 0x0 0x100>;
interrupts = <GIC_SPI 117 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru SCLK_UART1>, <&cru PCLK_UART1>;
clock-names = "baudclk", "apb_pclk";
reg-shift = <2>;
reg-io-width = <4>;
dmas = <&dmac0 2>, <&dmac0 3>;
pinctrl-names = "default";
pinctrl-0 = <&uart1m0_xfer>;
status = "disabled";
};
在内核代码中搜索compatible的两个字符串,可以找到snps,dw-apb-uart对应的8250驱动
3.修改serial.h
在/kernel/include/uapi//linux/serial.h中定义有serial_rs485结构体,在结构体中增加485的控制引脚信息
struct serial_rs485 {
__u32 flags; /* RS485 feature flags */
#define SER_RS485_ENABLED (1 << 0) /* If enabled */
#define SER_RS485_RTS_ON_SEND (1 << 1) /* Logical level for
RTS pin when
sending */
#define SER_RS485_RTS_AFTER_SEND (1 << 2) /* Logical level for
RTS pin after sent*/
#define SER_RS485_RX_DURING_TX (1 << 4)
#define SER_RS485_TERMINATE_BUS (1 << 5) /* Enable bus
termination
(if supported) */
__u32 delay_rts_before_send; /* Delay before send (milliseconds) */
__u32 delay_rts_after_send; /* Delay after send (milliseconds) */
__u32 padding[5]; /* Memory is cheap, new structs
are a royal PITA .. */
__u32 rts_gpio;//485的控制io
};
2.修改8250_dw.c
8250使用的是platform框架,找到dw8250_probe函数,增加头文件信息,在p->private_data = data后增加以下内容
#include <linux/gpio.h>
#include <linux/of_gpio.h>
//p->private_data = data后增加的内容
struct device_node *nd = dev->of_node;
int gpio_ctrl = of_get_named_gpio(nd, "485_ctrl_gpio", 0);
if (gpio_ctrl > 0)
{
p->rs485.flags = 0xabcd;
p->rs485.rts_gpio = gpio_ctrl;
gpio_direction_output(gpio_ctrl, 0);
gpio_set_value(gpio_ctrl, 1);
}
当匹配到串口信息后,提取设备树中485的控制信息,若有该属性则该串口为485,保存io号和标志,导出控制IO,将电平设置为默认读状态。
2.修改8250_port.c
通过struct uart_ops serial8250_pops可以知道,串口发送使用的函数serial8250_start_tx,调用了__start_tx,最后再调用serial8250_tx_chars,找到该函数,在函数中增加以下代码
if(0xabcd == port->rs485.flags)
{
if (gpio_get_value(port->rs485.rts_gpio) != 0)
{
gpio_set_value(port->rs485.rts_gpio, 0);
printk("this uart is 485, set rts gpio %d value 0\n", port->rs485.rts_gpio);
}
}
最后结束发送后
if(0xabcd == port->rs485.flags)
{
unsigned int lsr;
int loop_count = 200;
while (loop_count)
{
loop_count--;
lsr = serial_port_in(port, UART_LSR);
if (((lsr & UART_LSR_TEMT) == UART_LSR_TEMT))
break;
mdelay(1);
}
if (loop_count <= 0)
{
printk("timeout wait 485 send %d\n", port->rs485.rts_gpio);
}
gpio_set_value(port->rs485.rts_gpio, 1);
printk("this uart is 485,send over set rts gpio %d value 1\n", port->rs485.rts_gpio);
}
在串口发送前,若为485,则将控制引脚拉为发送状态,在发送后,循环查看是否发送完成,若发送完成则将引脚置回读状态,如此一来,就能实现在内核测控制方向切换,而不会因为延时造成数据丢失,经过测试此办法可行。
总结
以上则是本次调试RK3568的485时所遇到的问题、调试过程和解决办法,也参考了其他博主的帖子,综合之后做出此修改,希望对你们有所帮助。若有不对的地方也可指出。
标签:set,8250,RK3568,RS485,value,发送,fd,gpio,485 From: https://blog.csdn.net/qq_44179040/article/details/140825658