修改打包好的Alpine文件系统

之前介绍过嵌入式用的 Alpine Linux 发行版和如何移植,本文主要讲述对于打包好的系统压缩包,如何方便的添加删除软件包。

对于已经打包的 alpine 文件系统,经常需要根据需要添加或删除软件包,如果通过开发板操作再重新打包,不够方便,这里可以利用 static 编译的 apk 工具在宿主机上进行操作。

这还需要 archivemount 工具辅助,用于直接挂载压缩包:

1
sudo apt install archivemount

以及 apk.static 工具:

1
2
3
4
# 版本根据需要选择
wget https://dl-cdn.alpinelinux.org/alpine/latest-stable/main/x86_64/apk-tools-static-3.0.3-r1.apk
tar xvzf apk-tools-static-3.0.3-r1.apk
sudo mv sbin/apk.static /usr/local/bin

对于已经打包好的文件系统 alpine.tar.gz ,使用工具 archivemount 挂载:

1
2
mkdir ./mnt_alpine
sudo archivemount alpine.tar.gz ./mnt_alpine/

这里针对目标alpine架构是 armhf,操作命令如下:

1
2
3
4
# 添加包
sudo apk.static --root ./mnt_alpine/ --arch armhf add daemontools-encore
# 删除包
sudo apk.static --root ./mnt_alpine/ --arch armhf del daemontools-encore

而文件的编辑删除则可以直接通过挂载目录进行修改即可。

等待编辑结束,可以通过 umount 保存压缩包:

1
sudo umount ./mnt_alpine

同时,会创建一份挂载前的未修改副本,以 .orig 结尾。

这相对于在开发板上进行修改再打包会方便的多,尤其适合迭代开发过程中的更新修改。

RK3506 PWM oneshot 模式驱动修正

Rockchip RK3506 芯片 的 PWM 控制器支持 oneshot 模式,使用时,可以通过驱动模块、sysfs接口来配置周期、占空比和需要发送的数量(个数和重复次数),激活后硬件自动发送指定数量波形并结束。

这个功能非常适合用来变频控制外部设备,比如步进电机。

但是在实际使用时,通过 sysfs 调试,如果设置的周期过大(样例文档的 10us 没有问题),比如 100us 以上,当设置 enable 为 1 后,波形发送结束时,会出现驱动崩溃及内核panic(驱动=y构建),提示检测到 scheduling while atomic.

通过实际驱动 drivers/pwm/rockchip-pwm.c 中可以看 irq 函数,这里主要是针对 PWMv4:

 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
static irqreturn_t rockchip_pwm_irq_v4(int irq, void *data)
{
	struct rockchip_pwm_chip *pc = data;
	int val;
	irqreturn_t ret = IRQ_NONE;

	val = readl_relaxed(pc->base + INTSTS);
#ifdef CONFIG_PWM_ROCKCHIP_ONESHOT
	if (val & ONESHOT_END_INT) {
		struct pwm_state state;

		writel_relaxed(ONESHOT_END_INT, pc->base + INTSTS);

		/*
		 * Set pwm state to disabled when the oneshot mode finished.
		 */
		pwm_get_state(&pc->chip.pwms[0], &state);
		state.enabled = false;
		state.oneshot_count = 0;
		state.oneshot_repeat = 0;
		pwm_apply_state(&pc->chip.pwms[0], &state);

		rockchip_pwm_oneshot_callback(&pc->chip.pwms[0], &state);

		ret = IRQ_HANDLED;
	}
#endif
	if (val & CAP_LPR_INT) {

重点看 pwm_apply_state 这部分代码,这里是要清除已经设置的 oneshot 模式参数,保证结束时内部状态一致,但是 pwm_apply_state 这个函数内部明确标记 might_sleep() ,在 irq 中调用可能阻塞的函数会导致死锁等问题,而这个函数最终内部会调用rockhip_pwm及底层pinctrl一系列函数。

要解决这个崩溃问题,我们需要将 pwm_apply_state 从 top-half 移出去,丢给可以阻塞的上下文处理。驱动中最简单的修改办法,就是利用驱动结构体提供但未使用的 pwm_work 来实现。

具体就是删除这5行代码,新建一个处理函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
+static void rockchip_pwm_work(struct work_struct *work)
+{
+    struct rockchip_pwm_chip *pc = container_of(work, struct rockchip_pwm_chip, pwm_work.work);
+    struct pwm_device *pwm = &pc->chip.pwms[0];
+    struct pwm_state state;
+    pwm_get_state(pwm, &state);
+    state.enabled = false;
+    state.oneshot_count = 0;
+    state.oneshot_repeat = 0;
+    pwm_apply_state(pwm, &state);
+}
+

然后,在 irq 函数中,调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
	if (val & ONESHOT_END_INT) {
		struct pwm_state state;

		writel_relaxed(ONESHOT_END_INT, pc->base + INTSTS);

		/*
		 * Set pwm state to disabled when the oneshot mode finished.
		 */
+		schedule_delayed_work(&pc->pwm_work, 0);
		rockchip_pwm_oneshot_callback(&pc->chip.pwms[0], &state);

		ret = IRQ_HANDLED;
	}

并在 probe 函数中初始化:

1
2
3
4
5
6
7
8
static int rockchip_pwm_probe(struct platform_device *pdev)
 		goto err_pclk;
 	}
 
+       INIT_DELAYED_WORK(&pc->pwm_work, rockchip_pwm_work);
        rockchip_pwm_debugfs_init(pc);
 
        /* Keep the PWM clk enabled if the PWM appears to be up and running. */

以及 remove 函数中移除:

1
2
3
4
5
6
7
8
static int rockchip_pwm_remove(struct platform_device *pdev)
 	u32 val;
 
 	rockchip_pwm_debugfs_deinit(pc);
+	cancel_delayed_work_sync(&pc->pwm_work);
 
 	/*
 	 * For oneshot mode, it is needed to wait for bit PWM_ENABLE

需要注意的时,delayed_work 运行在进程上下文中,其调度受 CONFIG_HZ 的 jiffies影响,其执行可能存在较大的延迟(若干毫秒),如果业务应用对这个时间不敏感,则无所谓,如果敏感,则需要另外的修改方法,比如 threaded_irq 处理,但是这样改动相对较大,此处不再展开。

基于 x-macros 的命令行参数解析库实现

在之前文档讲述了一下 C X-Macros 的特性和一些应用场景,也提到在编译期可以确定的范式基本都可以通过 X-Macros 来辅助实现,比如命令行参数解析,这里就具体讲述如何实现。

具体代码在 github:macros_args

目标

因为使用过 argtable 这样的库,所以充分考虑了这种基于描述的命令行参数解析,但是也觉得还有一些繁琐,明明可以用一张表来表示所有参数,使用时却需要一个一个的初始化,而且库还有一些过于复杂的东西,比如特殊单位的处理支持。

设想中,这样的参数解析应该这样:

  1. 纯 header 文件实现,无 c 文件依赖;
  2. 表驱动配置解析参数,明确各个参数类型和特性;
  3. 支持参数重复,也就是同个参数传递多个值;
  4. 支持短参数和长参数,支持 literal/int/float/string 这样简单的参数类型;
  5. 尽量少依赖,不依赖动态内存分配,只适用少量 std 库和 getopt;
  6. 无全局状态,支持多实例;

考虑到实现复杂度,不依赖 std 库和 getopt 则会代码量巨大且不可维护,基本上就是需要利用标准库少量函数和getopt,来实现一个类似 argtable 这样的命令行参数解析。

思路

有了目标,就可以考虑如何实现了,本质上,就是如何将一个参数描述表转换为最终的 getopt 调用。

首先,需要考虑参数原型设计,基于以上,考虑设计4种基本参数类型:

  1. LIT, 也就是 literial,比如 -h -v 这样的开关参数,在内部应该是个整数表示是否出现及出现次数;
  2. INT,整数参数,内部用 int 保存即可。
  3. NUM,浮点参数,内部用 double 保存。
  4. STR,字符串参数,内部需要用 char *保存指针即可。 对于同参多值,可以将1个参数的情况作为多参数特例,所以表设计应该如下:
项目 描述
类型 参数类型,宏
最大数量 允许的最大数量,0 表示无限制(可以内部预设最大限制)
名称 参数在代码中的引用名称,需要符合 token 央视
短参名 类似 -t 的短参,用 'c' 这样表示,实际是整数,没有则写0
长参名 类似--long的长参,用"long"这样的字面量表示,没有则写 "" 空字符串
参数提示 类似 "<count>" 这样的字面量,用于帮助中提示带参的类型
默认值 参数默认值,对于多参数,只有第一个参数会被设置
描述 描述这个参数用来做什么

使用时,需要用户提供一个 X Macros 列表,我们再基于这个列表,来实现我们的必要拼装:

  1. 利用表生成一个结构体定义,用来存储我们的参数;
  2. 利用表来生成解析函数,用来解析命令行并存储结果;
  3. 利用表明来实现实例唯一,可以保证无全局状态多实例;
  4. 利用表进行约束检查,避免错误使用;

实现

具体实现可以参考代码,其中最重要的就是 _ARG_IF_ELSE 这个编译期条件宏,可以基于条件生成编译期代码,而非运行期:

1
2
3
4
#define _ARG_IF_ELSE_0(THEN, ELSE) ELSE
#define _ARG_IF_ELSE_1(THEN, ELSE) THEN
#define _ARG_IF_ELSE_IMPL(CONDITION, THEN, ELSE)    _ARG_CONCAT2(_ARG_IF_ELSE_, CONDITION)(THEN, ELSE)
#define _ARG_IF_ELSE(CONDITION, THEN, ELSE)         _ARG_IF_ELSE_IMPL(CONDITION, THEN, ELSE)

宏的实现就是简单的宏拼接,所以其应用很受限,只能基于宏展开时字面量数字来进行条件选择,参数必须是字面量0或1,如果直接使用,会很受限,需要配合_ARG_BOOL宏:

1
2
3
4
5
#define _ARG_CHECK_N(x, n, ...)     n
#define _ARG_PROBE(x)               x, 0,
#define _ARG_PROBE_0                _ARG_PROBE(~)
#define _ARG_CHECK(...)             _ARG_CHECK_N(__VA_ARGS__)
#define _ARG_BOOL(x)                _ARG_CHECK(_ARG_CONCAT2(_ARG_PROBE_, x), 1)

这个宏更绕一些,是根据宏字面量的0或非0来条件生成字面量0或1,这里用到了__VA_ARGS__ 展开参数数量,具体可以仔细揣摩。

有了这些宏,在结构体定义、静态检查和函数实现中,可以基于参数最大数量和参数类型来生成不同的编译期代码,避免编译器告警。

使用时,程序需要提供一个类似如下的参数列表宏,并利用库的宏来生成必要定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#define APP_ARGS_LIST(X) \
    ARG_LITX(X, help,       'h', "help",        "show help") \
    ARG_LIT1(X, print,      'p', "print",       "print arg struct") \
    ARG_LITX(X, verbose,    'v', "verbose",     "verbose") \
    ARG_NUM1(X, timeout,    't', "timeout",     "<secs>",  0.0,  "timeout in secs") \
    ARG_LIT1(X, longonly,   0,   "longonly",    "test long only arg") \
    ARG_LIT1(X, shortonly,  's', "",            "test short only arg") \
    ARG_STR1(X, from,       'f', "from",        "<file>",  "1.txt", "file path") \
    ARG_STRX(X, token,      'k', "token",       "<token>", "token0", "process tokens")

// 2. Generate essential
ARG_DEFINE(APP_ARGS_LIST);

然后在主函数中,可以进行解析:

1
2
3
4
5
// 3. Decleare struct to storage parse result
ARG_STRUCT_INIT(APP_ARGS_LIST, arg);
// 4. Parse argv with struct
// ret > 0 => optind, < 0 => error!
int ret = ARG_PARSE(APP_ARGS_LIST, argc, argv, &arg);

后续就可以利用arg成员,来判断参数,或者使用宏来打印参数帮助信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
if (ret < 0) {
    printf("Usage: %s", argv[0]);
    ARG_PRINT_SYNTAX(APP_ARGS_LIST);
    printf("\n");
    return ret;
}
// Use struct field to check result
// For LIT, use n_XXX
if (arg.n_print > 0) {
    printf("optind =%d\n", ret);
    ARG_STRUCT_PRINT(APP_ARGS_LIST, &arg);
    printf("\n");
}
if (arg.n_help > 0) {
    printf("Usage: %s", argv[0]);
    ARG_PRINT_SYNTAX(APP_ARGS_LIST);
    printf("\n\nExample:\n");
    ARG_PRINT_GLOSSARY(APP_ARGS_LIST, 30);
    return 0;
}

因为实现利用了大量宏,所以阅读起来会比较费劲,尤其是琐碎的条件处理。目前仍需要改进的部分就是利用短参字符字面量拼接getopt_long字符串参数时,通过手动去除单引号实现,这里之前考虑使用字面量而不是字符字面量,但是问题在于:

  1. #宏可以方便实现字符串字面量,但是很难基于宏实现字符字面量;
  2. getopt_long中即要字符字面量,也要字符串字面量;

手动去除虽然拙劣但十分有效。