1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

L2C-310 driverの中身を確認してみる

Posted at

■初めに

ARMのキャッシュというと外付けのPL-310が有名だろう(たぶんきっと)。
このあたりの振る舞いについて、確認をしていこうと思う。
(という作業メモ)

ソースコードの場所はこのあたり。

■l2x0_of_init()

▢device treeから該当nodeを探索

まず、初期化関数の中で、device treeの中で指定された識別子を持つノードを探し、メモリを確保してアクセス可能にしている。

cache-l2x0.c
int __init l2x0_of_init(u32 aux_val, u32 aux_mask)
{
        const struct l2c_init_data *data;
        struct device_node *np;
        struct resource res;
        u32 cache_id, old_aux;
        u32 cache_level = 2;

        np = of_find_matching_node(NULL, l2x0_ids);
        if (!np)
                return -ENODEV;

        if (of_address_to_resource(np, 0, &res))
                return -ENODEV;

        l2x0_base = ioremap(res.start, resource_size(&res));
        if (!l2x0_base)
                return -ENOMEM;

        l2x0_saved_regs.phy_base = res.start;

▢ l2c_init_dataのアドレスは...

通常の場合だと、l2cx_idx[]の中で関連付けられた, of_l2c310_dataになるけど、"arm,io-coherent"がtrueだったら、of_l2310_coherent_dataに置き換わる、と。

cache-l2x0.c
# define L2C_ID(name, fns) { .compatible = name, .data = (void *)&fns }
static const struct of_device_id l2x0_ids[] __initconst = {
        L2C_ID("arm,l210-cache", of_l2c210_data),
        L2C_ID("arm,l220-cache", of_l2c220_data),
        L2C_ID("arm,pl310-cache", of_l2c310_data),```
cache-l2x0.c
        data = of_match_node(l2x0_ids, np)->data;

        if (of_device_is_compatible(np, "arm,pl310-cache") &&
            of_property_read_bool(np, "arm,io-coherent"))
                data = &of_l2c310_coherent_data;

▢aux registerの書き換え

この関数の引数になっている、aux_valaux_maskが、L2X0_AUX_CTRLから読み出した値と一致しているかのテスト。一致していていない場合はコンソールに前後の値を表示。そうでなければ変化なしを表示。

cache-l2x0.c
        old_aux = readl_relaxed(l2x0_base + L2X0_AUX_CTRL);
        if (old_aux != ((old_aux & aux_mask) | aux_val)) {
                pr_warn("L2C: platform modifies aux control register: 0x%08x -> 0x%08x\n",
                        old_aux, (old_aux & aux_mask) | aux_val);
        } else if (aux_mask != ~0U && aux_val != 0) {
                pr_alert("L2C: platform provided aux values match the hardware, so have no effect.  Please remove them.\n");
        }

▢ device treeの記述のチェックとログ

簡単な属性確認

  • unidied cache指定があるか?
  • cache-levelの記述があるか?
  • cache-levelは2か?
cache-l2x0.c
        /* All L2 caches are unified, so this property should be specified */
        if (!of_property_read_bool(np, "cache-unified"))
                pr_err("L2C: device tree omits to specify unified cache\n");

        if (of_property_read_u32(np, "cache-level", &cache_level))
                pr_err("L2C: device tree omits to specify cache-level\n");

        if (cache_level != 2)
                pr_err("L2C: device tree specifies invalid cache level\n");

▢ デフォルト設定を保存

data->save()を使って、現在(そしてデフォルトの)ハードウェア設定を保存。

cache-l2x0.c
        /* Read back current (default) hardware configuration */
        if (data->save)
                data->save(l2x0_base);

▢ キャッシュ無効化と、新たな初期化

  • 設定変更をするために、一度L2Cを停止する
  • devicetreeから、aux_valと、aux_maskを取得
  • l2cからcache idの取得
    • 当該レジスタがない一部ハードのための回避策が、"cache_id_part_number_from_dt" なので、基本は無視してよい。
cache-l2x0.c
        /* L2 configuration can only be changed if the cache is disabled */
        if (!(readl_relaxed(l2x0_base + L2X0_CTRL) & L2X0_CTRL_EN))
                if (data->of_parse)
                        data->of_parse(np, &aux_val, &aux_mask);

        if (cache_id_part_number_from_dt)
                cache_id = cache_id_part_number_from_dt;
        else
                cache_id = readl_relaxed(l2x0_base + L2X0_CACHE_ID);

        return __l2c_init(data, aux_val, aux_mask, cache_id);
}

■__l2c_init()

▢callbackの退避

l2c_init_dataの中身を、コンテクスト外からも参照できるようにglobalにコピー。定義はココ。

cache-l2x0.c
static const struct l2c_init_data *l2x0_data;
cache-l2x0.c
static int __init __l2c_init(const struct l2c_init_data *data,
                             u32 aux_val, u32 aux_mask, u32 cache_id)
{
        struct outer_cache_fns fns;
        unsigned way_size_bits, ways;
        u32 aux, old_aux;

        /*
         * Save the pointer globally so that callbacks which do not receive
         * context from callers can access the structure.
         */
        l2x0_data = kmemdup(data, sizeof(*data), GFP_KERNEL);
        if (!l2x0_data)
                return -ENOMEM;

▢aux_val書き換え前のチェックと、auxの更新

    • aux_valの値に対して、aux_maskが正常な割り当てになっているかの確認。
  • aux_valが書き換わる可能性がありそうならばログを出力
  • auxを書き換え後の値とする
cache-l2x0.c
        /*
         * Sanity check the aux values.  aux_mask is the bits we preserve
         * from reading the hardware register, and aux_val is the bits we
         * set.
         */
        if (aux_val & aux_mask)
                pr_alert("L2C: platform provided aux values permit register corruption.\n");

        old_aux = aux = readl_relaxed(l2x0_base + L2X0_AUX_CTRL);
        aux &= aux_mask;
        aux |= aux_val;

        if (old_aux != aux)
                pr_warn("L2C: DT/platform modifies aux control register: 0x%08x -> 0x%08x\n",
                        old_aux, aux);

▢ number of waysの特定

キャッシュのwaysを特定する。

PL-310の場合だと、auxの16bit目に応じて、ways=16か8かが決まる。それ以外だと、auxの4bitにデータあるようなのでこれで決める。

cache-l2x0.c
        /* Determine the number of ways */
        switch (cache_id & L2X0_CACHE_ID_PART_MASK) {
        case L2X0_CACHE_ID_PART_L310:
                if ((aux_val | ~aux_mask) & (L2C_AUX_CTRL_WAY_SIZE_MASK | L310_AUX_CTRL_ASSOCIATIVITY_16))
                        pr_warn("L2C: DT/platform tries to modify or specify cache size\n");
                if (aux & (1 << 16))
                        ways = 16;
                else
                        ways = 8;
                break;

        case L2X0_CACHE_ID_PART_L210:
        case L2X0_CACHE_ID_PART_L220:
                ways = (aux >> 13) & 0xf;
                break;

        case AURORA_CACHE_ID:
                ways = (aux >> 13) & 0xf;
                ways = 2 << ((ways + 1) >> 2);
                break;

        default:
                /* Assume unknown chips have 8 ways */
                ways = 8;
                break;
        }

        l2x0_way_mask = (1 << ways) - 1;

▢L2Cサイズの特定

(way_size_0 << way_size_bits)は、1wayの大きさが求められる。これに、number of waysを乗じれば、全体のサイズが算出できる。

cache-l2x0.c
        /*
         * way_size_0 is the size that a way_size value of zero would be
         * given the calculation: way_size = way_size_0 << way_size_bits.
         * So, if way_size_bits=0 is reserved, but way_size_bits=1 is 16k,
         * then way_size_0 would be 8k.
         *
         * L2 cache size = number of ways * way size.
         */
        way_size_bits = (aux & L2C_AUX_CTRL_WAY_SIZE_MASK) >>
                        L2C_AUX_CTRL_WAY_SIZE_SHIFT;
        l2x0_size = ways * (data->way_size_0 << way_size_bits);

▢outer_cacheの更新

  • 他モジュールから、L2Cを制御するときに参照する、outer_cacheの更新を行う。
  • fixupが定義されていれば、このタイミングで呼び出す。
  • L2Cが停止しているはずなので、このタイミングで有効化する
cache-l2x0.c
        fns = data->outer_cache;
        fns.write_sec = outer_cache.write_sec;
        fns.configure = outer_cache.configure;
        if (data->fixup)
                data->fixup(l2x0_base, cache_id, &fns);
        /*
         * Check if l2x0 controller is already enabled.  If we are booting
         * in non-secure mode accessing the below registers will fault.
         */
        if (!(readl_relaxed(l2x0_base + L2X0_CTRL) & L2X0_CTRL_EN)) {
                l2x0_saved_regs.aux_ctrl = aux;

                data->enable(l2x0_base, data->num_lock);
        }

        outer_cache = fns;

▢Devicetree実装のために、再度レジスタ設定を保存する。

コメントにもある通り、「初期化処理が完了する前のレジスタを保存するのはおかしいけど、DT implementationでそれを決定しておかなければならないから…」らしい。

cache-l2x0.c
        /*
         * It is strange to save the register state before initialisation,
         * but hey, this is what the DT implementations decided to do.
         */
        if (data->save)
                data->save(l2x0_base);

▢ ログを出力

最後は、aux registerの値を出力して、終了!

cache-l2x0.c
        /* Re-read it in case some bits are reserved. */
        aux = readl_relaxed(l2x0_base + L2X0_AUX_CTRL);

        pr_info("%s cache controller enabled, %d ways, %d kB\n",
                data->type, ways, l2x0_size >> 10);
        pr_info("%s: CACHE_ID 0x%08x, AUX_CTRL 0x%08x\n",
                data->type, cache_id, aux);

        return 0;
}

■ of_l2c310_coherent_data

cache-l2x0.c
/*
 * This is a variant of the of_l2c310_data with .sync set to
 * NULL. Outer sync operations are not needed when the system is I/O
 * coherent, and potentially harmful in certain situations (PCIe/PL310
 * deadlock on Armada 375/38x due to hardware I/O coherency). The
 * other operations are kept because they are infrequent (therefore do
 * not cause the deadlock in practice) and needed for secondary CPU
 * boot and other power management activities.
 */

これは、,syncにNULLを指定した、of_l2c310_dataの亜種です。
Outer sync oprerationsは、system がI/O coherentである時には必要なく、また、特定条件下で潜在的に有害である場合があります。(ハードウェアI/O coherencyによる、Armada 375/38xにおけるPCIe/PL310 deadlock)

その他の操作は、頻度度が低く(すなわち、経験則的にdeaklockを引き起こさない)、secondary CPU起動やそのほかの電力制御activityに必要であるため、保持されます。

cache-l2x0.c
static const struct l2c_init_data of_l2c310_coherent_data __initconst = {
        .type = "L2C-310 Coherent",
        .way_size_0 = SZ_8K,
        .num_lock = 8,
        .of_parse = l2c310_of_parse,
        .enable = l2c310_enable,
        .fixup = l2c310_fixup,
        .save  = l2c310_save,
        .configure = l2c310_configure,
        .unlock = l2c310_unlock,
        .outer_cache = {
                .inv_range   = l2c210_inv_range,
                .clean_range = l2c210_clean_range,
                .flush_range = l2c210_flush_range,
                .flush_all   = l2c210_flush_all,
                .disable     = l2c310_disable,
                .resume      = l2c310_resume,
        },
};

■ l2c310_of_parse()

cache-l2x0.c
static void __init l2c310_of_parse(const struct device_node *np,
        u32 *aux_val, u32 *aux_mask)
{
        u32 data[3] = { 0, 0, 0 };
        u32 tag[3] = { 0, 0, 0 };
        u32 filter[2] = { 0, 0 };
        u32 assoc;
        u32 prefetch;
        u32 val;
        int ret;

▢ tag_latency/data_latency

cache-l2x0.c

        of_property_read_u32_array(np, "arm,tag-latency", tag, ARRAY_SIZE(tag));
        if (tag[0] && tag[1] && tag[2])
                l2x0_saved_regs.tag_latency =
                        L310_LATENCY_CTRL_RD(tag[0] - 1) |
                        L310_LATENCY_CTRL_WR(tag[1] - 1) |
                        L310_LATENCY_CTRL_SETUP(tag[2] - 1);

        of_property_read_u32_array(np, "arm,data-latency",
                                   data, ARRAY_SIZE(data));
        if (data[0] && data[1] && data[2])
                l2x0_saved_regs.data_latency =
                        L310_LATENCY_CTRL_RD(data[0] - 1) |
                        L310_LATENCY_CTRL_WR(data[1] - 1) |
                        L310_LATENCY_CTRL_SETUP(data[2] - 1);
Documentation/devicetree/bindings/arm/l2cc.txt
- arm,data-latency : Cycles of latency for Data RAM accesses. Specifies 3 cells of
  read, write and setup latencies. Minimum valid values are 1. Controllers
  without setup latency control should use a value of 0.
- arm,tag-latency : Cycles of latency for Tag RAM accesses. Specifies 3 cells of
  read, write and setup latencies. Controllers without setup latency control
  should use 0. Controllers without separate read and write Tag RAM latency
  values should only use the first cell.

arm.data-latency : Data RAM accessの時のkatencyのclock. read, write, setupの3cellである。最小値は1である。Controllerはsetup latency が無い場合には、0を利用する。
arm.tag-latency : Tag RAM accessの時のkatencyのclock. read, write, setupの3cellである。最小値は1である。Controllerはsetup latency が無い場合には、0を利用する。Tag RAM のread/writeを分離しないControllerでは最初の値だけが使われる。

▢ filter-ranges

cache-l2x0.c
        of_property_read_u32_array(np, "arm,filter-ranges",
                                   filter, ARRAY_SIZE(filter));
        if (filter[1]) {
                l2x0_saved_regs.filter_end =
                                        ALIGN(filter[0] + filter[1], SZ_1M);
                l2x0_saved_regs.filter_start = (filter[0] & ~(SZ_1M - 1))
                                        | L310_ADDR_FILTER_EN;
        }

▢ cacheパラメータ詳細(後述)

  • l2x0_cache_size_of_parse()の詳細は後述。
  • ここでは、cacheのAssociationの設定をしている。
cache-l2x0.c
        ret = l2x0_cache_size_of_parse(np, aux_val, aux_mask, &assoc, SZ_512K);
        if (!ret) {
                switch (assoc) {
                case 16:
                        *aux_val &= ~L2X0_AUX_CTRL_ASSOC_MASK;
                        *aux_val |= L310_AUX_CTRL_ASSOCIATIVITY_16;
                        *aux_mask &= ~L2X0_AUX_CTRL_ASSOC_MASK;
                        break;
                case 8:
                        *aux_val &= ~L2X0_AUX_CTRL_ASSOC_MASK;
                        *aux_mask &= ~L2X0_AUX_CTRL_ASSOC_MASK;
                        break;
                default:
                        pr_err("L2C-310 OF cache associativity %d invalid, only 8 or 16 permitted\n",
                               assoc);
                        break;
                }
        }

▢ shared-override

cache-l2x0.c
        if (of_property_read_bool(np, "arm,shared-override")) {
                *aux_val |= L2C_AUX_CTRL_SHARED_OVERRIDE;
                *aux_mask &= ~L2C_AUX_CTRL_SHARED_OVERRIDE;
        }
Documentation/devicetree/bindings/arm/l2cc.txt
- arm,shared-override : The default behavior of the pl310 cache controller with
  respect to the shareable attribute is to transform "normal memory
  non-cacheable transactions" into "cacheable no allocate" (for reads) or
  "write through no write allocate" (for writes).
  On systems where this may cause DMA buffer corruption, this property must be
  specified to indicate that such transforms are precluded.

pl310 cache controllerにshareable attributeが設定された場合のデフォルトの振る舞いは、”normal memory non-cacheable transactions"を、"cacheable no allocate"(読み込み時)もしくは、"write through no write allocate"(書き込み時)です。
これによってDMA bufferに問題が起きるシステムの場合、この属性を定義することで、上記変換処理を除外するように指示することができます。

▢ arm,double-linefill*

L3Cacheが接続されている場合に、doubleで送るかどうかの設定。
なので、L3Cが無い場合には何も変わらない...はず。

詳細については下記参照。

cache-l2x0.c
        prefetch = l2x0_saved_regs.prefetch_ctrl;

        ret = of_property_read_u32(np, "arm,double-linefill", &val);
        if (ret == 0) {
                if (val)
                        prefetch |= L310_PREFETCH_CTRL_DBL_LINEFILL;
                else
                        prefetch &= ~L310_PREFETCH_CTRL_DBL_LINEFILL;
        } else if (ret != -EINVAL) {
                pr_err("L2C-310 OF arm,double-linefill property value is missing\n");
        }

        ret = of_property_read_u32(np, "arm,double-linefill-incr", &val);
        if (ret == 0) {
                if (val)
                        prefetch |= L310_PREFETCH_CTRL_DBL_LINEFILL_INCR;
                else
                        prefetch &= ~L310_PREFETCH_CTRL_DBL_LINEFILL_INCR;
        } else if (ret != -EINVAL) {
                pr_err("L2C-310 OF arm,double-linefill-incr property value is missing\n");
        }

        ret = of_property_read_u32(np, "arm,double-linefill-wrap", &val);
        if (ret == 0) {
                if (!val)
                        prefetch |= L310_PREFETCH_CTRL_DBL_LINEFILL_WRAP;
                else
                        prefetch &= ~L310_PREFETCH_CTRL_DBL_LINEFILL_WRAP;
        } else if (ret != -EINVAL) {
                pr_err("L2C-310 OF arm,double-linefill-wrap property value is missing\n");
        }

▢ prefetch-offset / prefetch-drop

これらも、L3 Cache関連。詳細については下記参照。

cache-l2x0.c
        ret = of_property_read_u32(np, "arm,prefetch-drop", &val);
        if (ret == 0) {
                if (val)
                        prefetch |= L310_PREFETCH_CTRL_PREFETCH_DROP;
                else
                        prefetch &= ~L310_PREFETCH_CTRL_PREFETCH_DROP;
        } else if (ret != -EINVAL) {
                pr_err("L2C-310 OF arm,prefetch-drop property value is missing\n");
        }

        ret = of_property_read_u32(np, "arm,prefetch-offset", &val);
        if (ret == 0) {
                prefetch &= ~L310_PREFETCH_CTRL_OFFSET_MASK;
                prefetch |= val & L310_PREFETCH_CTRL_OFFSET_MASK;
        } else if (ret != -EINVAL) {
                pr_err("L2C-310 OF arm,prefetch-offset property value is missing\n");
        }

▢ prefetch-data/prefetch-instr

dataとinstructionのrefretchやるかどうか。

cache-l2x0.c

        ret = of_property_read_u32(np, "prefetch-data", &val);
        if (ret == 0) {
                if (val) {
                        prefetch |= L310_PREFETCH_CTRL_DATA_PREFETCH;
                        *aux_val |= L310_PREFETCH_CTRL_DATA_PREFETCH;
                } else {
                        prefetch &= ~L310_PREFETCH_CTRL_DATA_PREFETCH;
                        *aux_val &= ~L310_PREFETCH_CTRL_DATA_PREFETCH;
                }
                *aux_mask &= ~L310_PREFETCH_CTRL_DATA_PREFETCH;
        } else if (ret != -EINVAL) {
                pr_err("L2C-310 OF prefetch-data property value is missing\n");
        }

        ret = of_property_read_u32(np, "prefetch-instr", &val);
        if (ret == 0) {
                if (val) {
                        prefetch |= L310_PREFETCH_CTRL_INSTR_PREFETCH;
                        *aux_val |= L310_PREFETCH_CTRL_INSTR_PREFETCH;
                } else {
                        prefetch &= ~L310_PREFETCH_CTRL_INSTR_PREFETCH;
                        *aux_val &= ~L310_PREFETCH_CTRL_INSTR_PREFETCH;
                }
                *aux_mask &= ~L310_PREFETCH_CTRL_INSTR_PREFETCH;
        } else if (ret != -EINVAL) {
                pr_err("L2C-310 OF prefetch-instr property value is missing\n");
        }

        l2x0_saved_regs.prefetch_ctrl = prefetch;
}

■l2x0_cache_size_of_parse()

PL-310以外でも利用できる、キャッシュパラメータの解析部分。

cache-l2x0.c
/**
 * l2x0_cache_size_of_parse() - read cache size parameters from DT
 * @np: the device tree node for the l2 cache
 * @aux_val: pointer to machine-supplied auxilary register value, to
 * be augmented by the call (bits to be set to 1)
 * @aux_mask: pointer to machine-supplied auxilary register mask, to
 * be augmented by the call (bits to be set to 0)
 * @associativity: variable to return the calculated associativity in
 * @max_way_size: the maximum size in bytes for the cache ways
 */

▢cache-*属性の参照

以下の属性を読み出す。

  • [必須]cache-size -> cache_size
  • [必須]cache-sets -> sets
  • cacahe-block-size -> block_size
  • cache-line-size ^> line_size
cache-l2x0.c
static int __init l2x0_cache_size_of_parse(const struct device_node *np,
                                            u32 *aux_val, u32 *aux_mask,
                                            u32 *associativity,
                                            u32 max_way_size)

        u32 mask = 0, val = 0;
        u32 cache_size = 0, sets = 0;
        u32 way_size_bits = 1;
        u32 way_size = 0;
        u32 block_size = 0;
        u32 line_size = 0;

        of_property_read_u32(np, "cache-size", &cache_size);
        of_property_read_u32(np, "cache-sets", &sets);
        of_property_read_u32(np, "cache-block-size", &block_size);
        of_property_read_u32(np, "cache-line-size", &line_size);

        if (!cache_size || !sets)
                return -ENODEV;

▢blocksizeとline_sizeの確認

  • line_sizeが定義されていない
    • block_sizeが定義されていたら、line_size = block_size.
    • block_sizeが定義されていなかったら、デフォルトサイズCACHE_LINE_SIZEにする
  • line_sizeがCACHE_LINE_SIZEではない場合には、警告を表示する。
cache-l2x0.c
        /* All these l2 caches have the same line = block size actually */
        if (!line_size) {
                if (block_size) {
                        /* If linesize is not given, it is equal to blocksize */
                        line_size = block_size;
                } else {
                        /* Fall back to known size */
                        pr_warn("L2C OF: no cache block/line size given: "
                                "falling back to default size %d bytes\n",
                                CACHE_LINE_SIZE);
                        line_size = CACHE_LINE_SIZE;
                }
        }

        if (line_size != CACHE_LINE_SIZE)
                pr_warn("L2C OF: DT supplied line size %d bytes does "
                        "not match hardware line size of %d bytes\n",
                        line_size,
                        CACHE_LINE_SIZE);

▢way_sizeとassociativityの計算

このあたりの計算方法については、コメントに記載している通りですね...

cache-l2x0.c
        /*
         * Since:
         * set size = cache size / sets
         * ways = cache size / (sets * line size)
         * way size = cache size / (cache size / (sets * line size))
         * way size = sets * line size
         * associativity = ways = cache size / way size
         */
        way_size = sets * line_size;
        *associativity = cache_size / way_size;

        if (way_size > max_way_size) {
                pr_err("L2C OF: set size %dKB is too large\n", way_size);
                return -EINVAL;
        }

        pr_info("L2C OF: override cache size: %d bytes (%dKB)\n",
                cache_size, cache_size >> 10);
        pr_info("L2C OF: override line size: %d bytes\n", line_size);
        pr_info("L2C OF: override way size: %d bytes (%dKB)\n",
                way_size, way_size >> 10);
        pr_info("L2C OF: override associativity: %d\n", *associativity);

▢aux_val/maskに対する、way_size_bitの更新

  • way_sizeを10bit右シフトして、way_size_bitsに変換。
  • 範囲確認し、問題があれば失敗とする。
  • maskにWAY_SIZE_MASK変更のためのbitをあげる。
  • valに当該値を入れる。
  • aux_maskとaux_valに反映させる。
cache-l2x0.c
        /*
         * Calculates the bits 17:19 to set for way size:
         * 512KB -> 6, 256KB -> 5, ... 16KB -> 1
         */
        way_size_bits = ilog2(way_size >> 10) - 3;
        if (way_size_bits < 1 || way_size_bits > 6) {
                pr_err("L2C OF: cache way size illegal: %dKB is not mapped\n",
                       way_size);
                return -EINVAL;
        }

        mask |= L2C_AUX_CTRL_WAY_SIZE_MASK;
        val |= (way_size_bits << L2C_AUX_CTRL_WAY_SIZE_SHIFT);

        *aux_val &= ~mask;
        *aux_val |= val;
        *aux_mask &= ~mask;

        return 0;
}

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?