RK3506 uart 高波特率通信报错问题

在测试接收一个 1Mbps 串口数据的驱动上,RK3506 平台有时候启动后会有报错:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[   23.781504] ttyS4: maybe rx pin is low or baudrate is not correct!
[   23.783156] ttyS4: Frame error!
[   23.783175] ttyS4: maybe rx pin is low or baudrate is not correct!
[   23.785058] ttyS4: Frame error!
[   23.785077] ttyS4: maybe rx pin is low or baudrate is not correct!
[   23.786474] ttyS4: Frame error!
[   23.786483] ttyS4: maybe rx pin is low or baudrate is not correct!
[   23.788138] ttyS4: Frame error!
[   23.788146] ttyS4: maybe rx pin is low or baudrate is not correct!
[   23.789813] ttyS4: Frame error!
[   23.789825] ttyS4: maybe rx pin is low or baudrate is not correct!
[   23.791473] ttyS4: Frame error!
[   23.791483] ttyS4: maybe rx pin is low or baudrate is not correct!
[   23.793137] ttyS4: Frame error!

但这个错误出现几率不高,一旦出现会一直持续报错,断电重启后较大几率会消失。

所以高度怀疑是高波特率通信的电气兼容问题,通过查看内部 UART 时钟发现:

1
2
3
4
5
6
7
8
$ sudo cat /sys/kernel/debug/clk/sclk_uart1/clk_rate
24000000
$ sudo cat /sys/kernel/debug/clk/sclk_uart2/clk_rate
24000000
$ sudo cat /sys/kernel/debug/clk/sclk_uart3/clk_rate
24000000
$ sudo cat /sys/kernel/debug/clk/sclk_uart4/clk_rate
16000000

串口 uart1-3 都使用的115200波特率,时钟频率比使用1M波特率的 uart4 更高,这就奇怪了, 理论上通信波特率越高,时钟频率应该更高。

而且 uart 是16倍过采样,这种情况下,16M 时钟对于 1Mbps 的通信而言,没有任何余量, 任何电气上的影响延迟都会影响通信。所以怀疑是这个地方的问题。

源码文件在 drivers/tty/serial/8250/8250_dw.c中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static void dw8250_set_termios(struct uart_port *p, struct ktermios *termios,
			       struct ktermios *old)
{
#ifndef CONFIG_ARCH_ROCKCHIP
	unsigned long newrate = tty_termios_baud_rate(termios) * 16;
#endif
	struct dw8250_data *d = to_dw8250_data(p->private_data);
	long rate;
#ifdef CONFIG_ARCH_ROCKCHIP
	unsigned int baud = tty_termios_baud_rate(termios);
	unsigned int rate_temp, diff;
#endif
	int ret;

	clk_disable_unprepare(d->clk);
#ifdef CONFIG_ARCH_ROCKCHIP
	if (d->clk) {
		if (baud <= 115200)
			rate = 24000000;
		else if (baud == 230400)
			rate = baud * 16 * 2;
		else if (baud == 1152000)
			rate = baud * 16 * 2;
		else
			rate = baud * 16;

		ret = clk_set_rate(d->clk, rate);
		rate_temp = clk_get_rate(d->clk);
		diff = rate * 20 / 1000;

这里可以看到,115200 波特率一下时钟固定为24M,然后针对 230400/1152000 做了特殊处理,其他情况下,时钟频率就只有默认的16倍了,对于过采样而言,没有余量。

修改的话,比较简单,可以考虑将115200到1152000之间,统一做倍频处理:

1
2
3
4
5
6
		if (baud <= 115200)
			rate = 24000000;
		else if (baud <= 1152000)
			rate = baud * 16 * 2;
		else
			rate = baud * 16;

然后重新构建内核镜像并刷入,经过多次重启和断电测试,报错消失。

虽然可以通过优化电气连接等方法避免,但是某些情况也不是很方便做到,提高时钟频率算是一个 更为合适的做法。

IRQ 中 BH 处理的选择

这是一个内核模块开发的备忘,主要是中断处理中的 Botto-Half 处理框架选择有关。

(本文均为古法手搓,没有 AI 加成,如有错字语病实属正常)

今天在调试之前时间同步相关的内核模块时,连接一个基于 realtime 触发的 Trigger 模块到一个基于 gpio 输入的 Event 模块(通过 GPIOs 连接)时,Event 的时间戳相对 Trigger 的时间戳有较大延迟,平均在 20 微秒,理论上在这个 RK3506 平台上不应该有这么大的延迟,应该在 10 us 以内(GPIO 3-4us 加上内核调度延迟)。

那只能继续细看并分析了。

现有的内核模块时这样处理的,在 hrtimer / irq 回调中,会将时间戳和数据打包到 kfifo 中,然后通知可能阻塞等待新 数据的用户进程(用户空间poll),这里用到了 wake_up_interruptible(),实测发现当真的有程序在等待时,这个调用有接近10us 的开销,但是没有程序等待时,则开销很低。

但是使用这个模块必然需要程序来监听读取数据,那么简单,把这个调用放到 Bottom-Half 就行了。因为 wake_up 没有 实时性要求,所以直接选择了 workqueue 来做,workqueue 工作在 process context 下,实质在 kworker 中,调用阻塞接口也没有关系。

改动:

1
2
3
4
5
6
7
8
# 新增结构
struct work_struct wake_work
# 初始化
INIT_WORK(...)
# irq 调用
schedule_work(&ctx->wake_work)
# 销毁
cancel_work_sync(...)

然后测试,Event 与 Trigger 延迟基本降低在 12 us 左右,有改善。但是多次测试下来发现,有时候延迟还是会上升到 20+ us ,而且一旦在这个级别,延迟会稳定在这个级别。

这就有点奇怪了,在负载变化不变的情况下,不应该是这个现象。既然好复现,就直接输出调试时间差。

结果发现,Trigger 的 irq 全程差不多要 10-21 us 左右,而且基本上都超过 10us 了,这不符合预期。因为实际硬件上 Trigger 使用的 HARD hrtimer 模式,也就是在回调中 irq 会被禁用,而且 Event 的 irq 与 Trigger 在同一个 CPU 上,那么这时候 irq 就是串行的,意味着 Trigger 处理耗时,那么 Event 的延迟必然增大。而且这里有时候延迟 20+ 与 直接在 irq 中调用 wake_up 就很相似了。

继续测量时间,发现 Trigger 的回调中,schedule_work 的耗时很不稳定,间隔的忽高忽低,高可以到 10+ us(含调试代码),低的时候只有几百 ns。这也与预期不符合,直觉说明 schedule_work 有隐藏开销。

schedule_work 底层实现上,因为依赖内核进程,那么需要通知/激活对应的进程,如果进程在休眠,则还需要调度唤醒,这里面就会有开销。这跟 tasklet 很不一样,tasklet 基于 softirq 实现,tasklet_schedule 只需要标记下,后续 softirq 会直接执行对应任务,要求是任务里不能阻塞而已。

从这一点分析,在 Trigger 回调之后,唤醒的 kworker 被调度运行,这时 Event irq 触发,那么又需要切换上下文进入中断。本身 schedule_work 的最差开销加上两次切换,那么的确会恶化延迟。

继续修改:

1
2
3
4
5
6
7
8
# 新增结构
struct tasklet_struct wake_tasklet
# 初始化
tasklet_init(...)
# irq 调用
tasklet_schedule(&ctx->wake_tasklet)
# 销毁
tasklet_kill(...)

继续测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Trigger 数据:                Event 数据:
11687: 1781254159 000002905  | 0 11687: 1781254159 000013405
11688: 1781254159 200002550  | 0 11688: 1781254159 200009550
11689: 1781254159 400004237  | 0 11689: 1781254159 400014153
11690: 1781254159 600004173  | 0 11690: 1781254159 600011465
11691: 1781254159 800002943  | 0 11691: 1781254159 800009652
11692: 1781254160 000003172  | 0 11692: 1781254160 000009589
11693: 1781254160 200003984  | 0 11693: 1781254160 200010400
11694: 1781254160 400003629  | 0 11694: 1781254160 400010337
11695: 1781254160 600003565  | 0 11695: 1781254160 600009690
11696: 1781254160 800003794  | 0 11696: 1781254160 800011377
11697: 1781254161 000003439  | 0 11697: 1781254161 000013939
11698: 1781254161 200003667  | 0 11698: 1781254161 200011834
11699: 1781254161 400005646  | 0 11699: 1781254161 400013521
11700: 1781254161 600003249  | 0 11700: 1781254161 600009666
11701: 1781254161 800004644  | 0 11701: 1781254161 800012811

总体上看,延迟要好很多,平均在 10 us 以内了,也符合预期。

总体分析,因为 hrtimer HARD 模式下,回调会禁用 irq ,那么整体会串行(同一个 cpu 下),前者处理时间越长,那么后者延迟就越大。 第一次分析出了部分原因,但是使用了不合适的 BH 处理框架。

虽然内核现在开始冻结 tasklet 的使用,并且提供了 BH workqueue 来代替(kernel 6.9+),但是可见很长时间以内,tasklet 还是可以可靠使用的,尤其针对这种时间敏感型模块。

使用 safe-rm 避免意外删除

命令行操作中,难以避免 rm 命令的使用,如果脑袋昏了或者慢摆拍,很容易误删重要的文件,尤其是用户目录下的,不像根目录那样需要权限确认。

这里可以使用 safe-rm 工具来防止这种意外情况,在对被保护的目录/文件进行删除时,会打印提示并忽略。

安装及配置

Debian/Ubuntu 可以通过命令 apt install safe-rm 来安装,这个工具本质是对 rm 的一层包装,加了目标目录的检查。

安装后当前用户的默认 rm 指令就会指向 safe-rm /usr/share/safe-rm/bin/rm ,这个可以通过 which rm 来确认。接下来,我们可以通过配置 全局和用户级别的 safe-rm 文件来保护需要的目录和文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 全局配置文件路径: /etc/safe-rm.conf
# 这里已经有一些预制的目录,主要是重要的系统级别目录
/
/bin
/boot
/dev
/etc
/home
/initrd
/lib
/proc
/root
/sbin
/sys
/usr
/usr/bin
/usr/include
/usr/lib
/usr/local
/usr/local/bin
/usr/local/include
/usr/local/sbin
/usr/local/share
/usr/sbin
/usr/share
/usr/src
/var
# 在下面,我们可以添加自己需要保护的全局目录/文件,可以使用通配符
/home/*
/home/*/Documents
/home/*/.config
/home/*/.ssh
/home/*/.mozilla

用户级别配置文件在 ~/.config/safe-rm :

1
2
3
# 该配置仅对当前用户有效
# 建议与全局配置一样,用绝对路径配合通配符
/home/*/.local

然后在删除时,如果不小心误写,也可以提醒并忽略:

1
2
rm -rf /
safe-rm: Skipping /.

应用到 sudo 环境

以上配置对当前用户有效,但对 root 命令中调用的 rm 却无效,因为 sudo 会使用安全的环境,safe-rm 无法优先于 rm 调用。

方法是通过命令 sudo visudo 编辑sudoers 文件,找到 Defaults secure_path 行,将 safe-rm 加入:

1
2
# 将 /usr/share/safe-rm/bin 加入 secure_path 中并优先
Defaults    secure_path="/usr/share/safe-rm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

然后通过命令 sudo which rm 来确认是否生效:

1
2
sudo which rm
/usr/share/safe-rm/bin/rm