// SPDX-License-Identifier: GPL-2.0 /* * Serial Attached SCSI (SAS) Expander discovery and configuration * * Copyright (C) 2005 Adaptec, Inc. All rights reserved. * Copyright (C) 2005 Luben Tuikov <luben_tuikov@adaptec.com> * * This file is licensed under GPLv2.
*/
staticchar sas_route_char(struct domain_device *dev, struct ex_phy *phy)
{ switch (phy->routing_attr) { case TABLE_ROUTING: if (dev->ex_dev.t2t_supp) return'U'; else return'T'; case DIRECT_ROUTING: return'D'; case SUBTRACTIVE_ROUTING: return'S'; default: return'?';
}
}
staticenum sas_device_type to_dev_type(struct discover_resp *dr)
{ /* This is detecting a failure to transmit initial dev to host * FIS as described in section J.5 of sas-2 r16
*/ if (dr->attached_dev_type == SAS_PHY_UNUSED && dr->attached_sata_dev &&
dr->linkrate >= SAS_LINK_RATE_1_5_GBPS) return SAS_SATA_PENDING; else return dr->attached_dev_type;
}
if (new_phy) { if (WARN_ON_ONCE(test_bit(SAS_HA_ATA_EH_ACTIVE, &ha->state))) return;
phy->phy = sas_phy_alloc(&rphy->dev, phy_id);
/* FIXME: error_handling */
BUG_ON(!phy->phy);
}
switch (disc_resp->result) { case SMP_RESP_PHY_VACANT:
phy->phy_state = PHY_VACANT; break; default:
phy->phy_state = PHY_NOT_PRESENT; break; case SMP_RESP_FUNC_ACC:
phy->phy_state = PHY_EMPTY; /* do not know yet */ break;
}
/* check if anything important changed to squelch debug */
dev_type = phy->attached_dev_type;
linkrate = phy->linkrate;
memcpy(sas_addr, phy->attached_sas_addr, SAS_ADDR_SIZE);
/* Handle vacant phy - rest of dr data is not valid so skip it */ if (phy->phy_state == PHY_VACANT) {
memset(phy->attached_sas_addr, 0, SAS_ADDR_SIZE);
phy->attached_dev_type = SAS_PHY_UNUSED; if (!test_bit(SAS_HA_ATA_EH_ACTIVE, &ha->state)) {
phy->phy_id = phy_id; goto skip;
} else goto out;
}
phy->attached_dev_type = to_dev_type(dr); if (test_bit(SAS_HA_ATA_EH_ACTIVE, &ha->state)) goto out;
phy->phy_id = phy_id;
phy->linkrate = dr->linkrate;
phy->attached_sata_host = dr->attached_sata_host;
phy->attached_sata_dev = dr->attached_sata_dev;
phy->attached_sata_ps = dr->attached_sata_ps;
phy->attached_iproto = dr->iproto << 1;
phy->attached_tproto = dr->tproto << 1; /* help some expanders that fail to zero sas_address in the 'no * device' case
*/ if (phy->attached_dev_type == SAS_PHY_UNUSED)
memset(phy->attached_sas_addr, 0, SAS_ADDR_SIZE); else
memcpy(phy->attached_sas_addr, dr->attached_sas_addr, SAS_ADDR_SIZE);
phy->attached_phy_id = dr->attached_phy_id;
phy->phy_change_count = dr->change_count;
phy->routing_attr = dr->routing_attr;
phy->virtual = dr->virtual;
phy->last_da_index = -1;
skip: if (new_phy) if (sas_phy_add(phy->phy)) {
sas_phy_free(phy->phy); return;
}
out: switch (phy->attached_dev_type) { case SAS_SATA_PENDING:
type = "stp pending"; break; case SAS_PHY_UNUSED:
type = "no device"; break; case SAS_END_DEVICE: if (phy->attached_iproto) { if (phy->attached_tproto)
type = "host+target"; else
type = "host";
} else { if (dr->attached_sata_dev)
type = "stp"; else
type = "ssp";
} break; case SAS_EDGE_EXPANDER_DEVICE: case SAS_FANOUT_EXPANDER_DEVICE:
type = "smp"; break; default:
type = "unknown";
}
/* this routine is polled by libata error recovery so filter * unimportant messages
*/ if (new_phy || phy->attached_dev_type != dev_type ||
phy->linkrate != linkrate ||
SAS_ADDR(phy->attached_sas_addr) != SAS_ADDR(sas_addr)) /* pass */; else return;
/* if the attached device type changed and ata_eh is active, * make sure we run revalidation when eh completes (see: * sas_enable_revalidation)
*/ if (test_bit(SAS_HA_ATA_EH_ACTIVE, &ha->state))
set_bit(DISCE_REVALIDATE_DOMAIN, &dev->port->disc.pending);
/* check if we have an existing attached ata device on this expander phy */ struct domain_device *sas_ex_to_ata(struct domain_device *ex_dev, int phy_id)
{ struct ex_phy *ex_phy = &ex_dev->ex_dev.ex_phy[phy_id]; struct domain_device *dev; struct sas_rphy *rphy;
if (!ex_phy->port) return NULL;
rphy = ex_phy->port->rphy; if (!rphy) return NULL;
res = smp_execute_task(dev, rps_req, RPS_REQ_SIZE,
rps_resp, RPS_RESP_SIZE);
/* 0x34 is the FIS type for the D2H fis. There's a potential * standards cockup here. sas-2 explicitly specifies the FIS * should be encoded so that FIS type is in resp[24]. * However, some expanders endian reverse this. Undo the
* reversal here */ if (!res && resp[27] == 0x34 && resp[24] != 0x34) { int i;
for (i = 0; i < 5; i++) { int j = 24 + (i*4);
u8 a, b;
a = resp[j + 0];
b = resp[j + 1];
resp[j + 0] = resp[j + 3];
resp[j + 1] = resp[j + 2];
resp[j + 2] = b;
resp[j + 3] = a;
}
}
res = sas_notify_lldd_dev_found(child); if (res) {
pr_notice("notify lldd for device %016llx at %016llx:%02d returned 0x%x\n",
SAS_ADDR(child->sas_addr),
SAS_ADDR(parent->sas_addr), phy_id, res);
sas_rphy_free(child->rphy);
list_del(&child->disco_list_node); return res;
}
/* See if this phy is part of a wide port */ staticbool sas_ex_join_wide_port(struct domain_device *parent, int phy_id)
{ struct ex_phy *phy = &parent->ex_dev.ex_phy[phy_id]; int i;
for (i = 0; i < parent->ex_dev.num_phys; i++) { struct ex_phy *ephy = &parent->ex_dev.ex_phy[i];
staticint sas_ex_discover_dev(struct domain_device *dev, int phy_id)
{ struct expander_device *ex = &dev->ex_dev; struct ex_phy *ex_phy = &ex->ex_phy[phy_id]; struct domain_device *child = NULL; int res = 0;
/* Phy state */ if (ex_phy->linkrate == SAS_SATA_SPINUP_HOLD) { if (!sas_smp_phy_control(dev, phy_id, PHY_FUNC_LINK_RESET, NULL))
res = sas_ex_phy_discover(dev, phy_id); if (res) return res;
}
/* Parent and domain coherency */ if (!dev->parent && sas_phy_match_port_addr(dev->port, ex_phy)) {
sas_ex_add_parent_port(dev, phy_id); return 0;
} if (dev->parent && sas_phy_match_dev_addr(dev->parent, ex_phy)) {
sas_ex_add_parent_port(dev, phy_id); if (ex_phy->routing_attr == TABLE_ROUTING)
sas_configure_phy(dev, phy_id, dev->port->sas_addr, 1); return 0;
}
if (sas_dev_present_in_domain(dev->port, ex_phy->attached_sas_addr))
sas_ex_disable_port(dev, ex_phy->attached_sas_addr);
res = sas_configure_routing(dev, ex_phy->attached_sas_addr); if (res) {
pr_notice("configure routing for dev %016llx reported 0x%x. Forgotten\n",
SAS_ADDR(ex_phy->attached_sas_addr), res);
sas_disable_routing(dev, ex_phy->attached_sas_addr); return res;
}
if (sas_ex_join_wide_port(dev, phy_id)) {
pr_debug("Attaching ex phy%02d to wide port %016llx\n",
phy_id, SAS_ADDR(ex_phy->attached_sas_addr)); return res;
}
switch (ex_phy->attached_dev_type) { case SAS_END_DEVICE: case SAS_SATA_PENDING:
child = sas_ex_discover_end_dev(dev, phy_id); break; case SAS_FANOUT_EXPANDER_DEVICE: if (SAS_ADDR(dev->port->disc.fanout_sas_addr)) {
pr_debug("second fanout expander %016llx phy%02d attached to ex %016llx phy%02d\n",
SAS_ADDR(ex_phy->attached_sas_addr),
ex_phy->attached_phy_id,
SAS_ADDR(dev->sas_addr),
phy_id);
sas_ex_disable_phy(dev, phy_id); return res;
} else
memcpy(dev->port->disc.fanout_sas_addr,
ex_phy->attached_sas_addr, SAS_ADDR_SIZE);
fallthrough; case SAS_EDGE_EXPANDER_DEVICE:
child = sas_ex_discover_expander(dev, phy_id); break; default: break;
}
if (!child)
pr_notice("ex %016llx phy%02d failed to discover\n",
SAS_ADDR(dev->sas_addr), phy_id); return res;
}
sas_ex_disable_port(child, s2);
}
}
} return 0;
} /** * sas_ex_discover_devices - discover devices attached to this expander * @dev: pointer to the expander domain device * @single: if you want to do a single phy, else set to -1; * * Configure this expander for use with its devices and register the * devices of this expander.
*/ staticint sas_ex_discover_devices(struct domain_device *dev, int single)
{ struct expander_device *ex = &dev->ex_dev; int i = 0, end = ex->num_phys; int res = 0;
if (0 <= single && single < end) {
i = single;
end = i+1;
}
for ( ; i < end; i++) { struct ex_phy *ex_phy = &ex->ex_phy[i];
switch (ex_phy->linkrate) { case SAS_PHY_DISABLED: case SAS_PHY_RESET_PROBLEM: case SAS_SATA_PORT_SELECTOR: continue; default:
res = sas_ex_discover_dev(dev, i); if (res) break; continue;
}
}
if (!res)
sas_check_level_subtractive_boundary(dev);
if (SAS_ADDR(disc->fanout_sas_addr) != 0) {
res = -ENODEV;
pr_warn("edge ex %016llx phy S:%02d <--> edge ex %016llx phy S:%02d, while there is a fanout ex %016llx\n",
SAS_ADDR(parent->sas_addr),
parent_phy->phy_id,
SAS_ADDR(child->sas_addr),
child_phy->phy_id,
SAS_ADDR(disc->fanout_sas_addr));
} elseif (SAS_ADDR(disc->eeds_a) == 0) {
memcpy(disc->eeds_a, parent->sas_addr, SAS_ADDR_SIZE);
memcpy(disc->eeds_b, child->sas_addr, SAS_ADDR_SIZE);
} elseif (!sas_eeds_valid(parent, child)) {
res = -ENODEV;
pr_warn("edge ex %016llx phy%02d <--> edge ex %016llx phy%02d link forms a third EEDS!\n",
SAS_ADDR(parent->sas_addr),
parent_phy->phy_id,
SAS_ADDR(child->sas_addr),
child_phy->phy_id);
}
staticint sas_check_parent_topology(struct domain_device *child)
{ struct expander_device *parent_ex; int i; int res = 0;
if (!dev_parent_is_expander(child)) return 0;
parent_ex = &child->parent->ex_dev;
for (i = 0; i < parent_ex->num_phys; i++) { struct ex_phy *parent_phy = &parent_ex->ex_phy[i];
if (parent_phy->phy_state == PHY_VACANT ||
parent_phy->phy_state == PHY_NOT_PRESENT) continue;
if (!sas_phy_match_dev_addr(child, parent_phy)) continue;
switch (child->parent->dev_type) { case SAS_EDGE_EXPANDER_DEVICE: if (sas_check_edge_expander_topo(child, parent_phy))
res = -ENODEV; break; case SAS_FANOUT_EXPANDER_DEVICE: if (sas_check_fanout_expander_topo(child, parent_phy))
res = -ENODEV; break; default: break;
}
}
return res;
}
#define RRI_REQ_SIZE 16 #define RRI_RESP_SIZE 44
staticint sas_configure_present(struct domain_device *dev, int phy_id,
u8 *sas_addr, int *index, int *present)
{ int i, res = 0; struct expander_device *ex = &dev->ex_dev; struct ex_phy *phy = &ex->ex_phy[phy_id];
u8 *rri_req;
u8 *rri_resp;
*present = 0;
*index = 0;
rri_req = alloc_smp_req(RRI_REQ_SIZE); if (!rri_req) return -ENOMEM;
rri_resp = alloc_smp_resp(RRI_RESP_SIZE); if (!rri_resp) {
kfree(rri_req); return -ENOMEM;
}
res = smp_execute_task(dev, cri_req, CRI_REQ_SIZE, cri_resp,
CRI_RESP_SIZE); if (res) goto out;
res = cri_resp[2]; if (res == SMP_RESP_NO_INDEX) {
pr_warn("overflow of indexes: dev %016llx phy%02d index 0x%x\n",
SAS_ADDR(dev->sas_addr), phy_id, index);
}
out:
kfree(cri_req);
kfree(cri_resp); return res;
}
staticint sas_configure_phy(struct domain_device *dev, int phy_id,
u8 *sas_addr, int include)
{ int index; int present; int res;
res = sas_configure_present(dev, phy_id, sas_addr, &index, &present); if (res) return res; if (include ^ present) return sas_configure_set(dev, phy_id, sas_addr, index,
include);
return res;
}
/** * sas_configure_parent - configure routing table of parent * @parent: parent expander * @child: child expander * @sas_addr: SAS port identifier of device directly attached to child * @include: whether or not to include @child in the expander routing table
*/ staticint sas_configure_parent(struct domain_device *parent, struct domain_device *child,
u8 *sas_addr, int include)
{ struct expander_device *ex_parent = &parent->ex_dev; int res = 0; int i;
if (parent->parent) {
res = sas_configure_parent(parent->parent, parent, sas_addr,
include); if (res) return res;
}
if (ex_parent->conf_route_table == 0) {
pr_debug("ex %016llx has self-configuring routing table\n",
SAS_ADDR(parent->sas_addr)); return 0;
}
for (i = 0; i < ex_parent->num_phys; i++) { struct ex_phy *phy = &ex_parent->ex_phy[i];
if ((phy->routing_attr == TABLE_ROUTING) &&
sas_phy_match_dev_addr(child, phy)) {
res = sas_configure_phy(parent, i, sas_addr, include); if (res) return res;
}
}
return res;
}
/** * sas_configure_routing - configure routing * @dev: expander device * @sas_addr: port identifier of device directly attached to the expander device
*/ staticint sas_configure_routing(struct domain_device *dev, u8 *sas_addr)
{ if (dev->parent) return sas_configure_parent(dev->parent, dev, sas_addr, 1); return 0;
}
staticint sas_get_phy_discover(struct domain_device *dev, int phy_id, struct smp_disc_resp *disc_resp)
{ int res;
u8 *disc_req;
disc_req = alloc_smp_req(DISCOVER_REQ_SIZE); if (!disc_req) return -ENOMEM;
disc_req[1] = SMP_DISCOVER;
disc_req[9] = phy_id;
res = smp_execute_task(dev, disc_req, DISCOVER_REQ_SIZE,
disc_resp, DISCOVER_RESP_SIZE); if (res) goto out; if (disc_resp->result != SMP_RESP_FUNC_ACC)
res = disc_resp->result;
out:
kfree(disc_req); return res;
}
staticint sas_get_phy_change_count(struct domain_device *dev, int phy_id, int *pcc)
{ int res; struct smp_disc_resp *disc_resp;
disc_resp = alloc_smp_resp(DISCOVER_RESP_SIZE); if (!disc_resp) return -ENOMEM;
res = sas_get_phy_discover(dev, phy_id, disc_resp); if (!res)
*pcc = disc_resp->disc.change_count;
kfree(disc_resp); return res;
}
int sas_get_phy_attached_dev(struct domain_device *dev, int phy_id,
u8 *sas_addr, enum sas_device_type *type)
{ int res; struct smp_disc_resp *disc_resp;
disc_resp = alloc_smp_resp(DISCOVER_RESP_SIZE); if (!disc_resp) return -ENOMEM;
res = sas_get_phy_discover(dev, phy_id, disc_resp); if (res == 0)
sas_get_sas_addr_and_dev_type(disc_resp, sas_addr, type);
kfree(disc_resp); return res;
}
staticint sas_find_bcast_phy(struct domain_device *dev, int *phy_id, int from_phy, bool update)
{ struct expander_device *ex = &dev->ex_dev; int res = 0; int i;
for (i = from_phy; i < ex->num_phys; i++) { int phy_change_count = 0;
res = sas_get_phy_change_count(dev, i, &phy_change_count); switch (res) { case SMP_RESP_PHY_VACANT: case SMP_RESP_NO_PHY: continue; case SMP_RESP_FUNC_ACC: break; default: return res;
}
if (phy_change_count != ex->ex_phy[i].phy_change_count) { if (update)
ex->ex_phy[i].phy_change_count =
phy_change_count;
*phy_id = i; return 0;
}
} return 0;
}
staticint sas_get_ex_change_count(struct domain_device *dev, int *ecc)
{ int res;
u8 *rg_req; struct smp_rg_resp *rg_resp;
rg_req = alloc_smp_req(RG_REQ_SIZE); if (!rg_req) return -ENOMEM;
rg_resp = alloc_smp_resp(RG_RESP_SIZE); if (!rg_resp) {
kfree(rg_req); return -ENOMEM;
}
rg_req[1] = SMP_REPORT_GENERAL;
res = smp_execute_task(dev, rg_req, RG_REQ_SIZE, rg_resp,
RG_RESP_SIZE); if (res) goto out; if (rg_resp->result != SMP_RESP_FUNC_ACC) {
res = rg_resp->result; goto out;
}
*ecc = be16_to_cpu(rg_resp->rg.change_count);
out:
kfree(rg_resp);
kfree(rg_req); return res;
} /** * sas_find_bcast_dev - find the device issue BROADCAST(CHANGE). * @dev:domain device to be detect. * @src_dev: the device which originated BROADCAST(CHANGE). * * Add self-configuration expander support. Suppose two expander cascading, * when the first level expander is self-configuring, hotplug the disks in * second level expander, BROADCAST(CHANGE) will not only be originated * in the second level expander, but also be originated in the first level * expander (see SAS protocol SAS 2r-14, 7.11 for detail), it is to say, * expander changed count in two level expanders will all increment at least * once, but the phy which chang count has changed is the source device which * we concerned.
*/
staticint sas_find_bcast_dev(struct domain_device *dev, struct domain_device **src_dev)
{ struct expander_device *ex = &dev->ex_dev; int ex_change_count = -1; int phy_id = -1; int res; struct domain_device *ch;
res = sas_get_ex_change_count(dev, &ex_change_count); if (res) goto out; if (ex_change_count != -1 && ex_change_count != ex->ex_change_count) { /* Just detect if this expander phys phy change count changed, * in order to determine if this expander originate BROADCAST, * and do not update phy change count field in our structure.
*/
res = sas_find_bcast_phy(dev, &phy_id, 0, false); if (phy_id != -1) {
*src_dev = dev;
ex->ex_change_count = ex_change_count;
pr_info("ex %016llx phy%02d change count has changed\n",
SAS_ADDR(dev->sas_addr), phy_id); return res;
} else
pr_info("ex %016llx phys DID NOT change\n",
SAS_ADDR(dev->sas_addr));
}
list_for_each_entry(ch, &ex->children, siblings) { if (dev_is_expander(ch->dev_type)) {
res = sas_find_bcast_dev(ch, src_dev); if (*src_dev) return res;
}
}
out: return res;
}
/* treat device directed resets as flutter, if we went * SAS_END_DEVICE to SAS_SATA_PENDING the link needs recovery
*/ if ((old == SAS_SATA_PENDING && new == SAS_END_DEVICE) ||
(old == SAS_END_DEVICE && new == SAS_SATA_PENDING)) returntrue;
returnfalse;
}
staticint sas_rediscover_dev(struct domain_device *dev, int phy_id, bool last, int sibling)
{ struct expander_device *ex = &dev->ex_dev; struct ex_phy *phy = &ex->ex_phy[phy_id]; enum sas_device_type type = SAS_PHY_UNUSED; struct smp_disc_resp *disc_resp;
u8 sas_addr[SAS_ADDR_SIZE]; char msg[80] = ""; int res;
if (!last)
sprintf(msg, ", part of a wide port with phy%02d", sibling);
/* we always have to delete the old device when we went here */
pr_info("ex %016llx phy%02d replace %016llx\n",
SAS_ADDR(dev->sas_addr), phy_id,
SAS_ADDR(phy->attached_sas_addr));
sas_unregister_devs_sas_addr(dev, phy_id, last);
res = sas_discover_new(dev, phy_id);
out_free_resp:
kfree(disc_resp); return res;
}
/** * sas_rediscover - revalidate the domain. * @dev:domain device to be detect. * @phy_id: the phy id will be detected. * * NOTE: this process _must_ quit (return) as soon as any connection * errors are encountered. Connection recovery is done elsewhere. * Discover process only interrogates devices in order to discover the * domain.For plugging out, we un-register the device only when it is * the last phy in the port, for other phys in this port, we just delete it * from the port.For inserting, we do discovery when it is the * first phy,for other phys in this port, we add it to the port to * forming the wide-port.
*/ staticint sas_rediscover(struct domain_device *dev, constint phy_id)
{ struct expander_device *ex = &dev->ex_dev; struct ex_phy *changed_phy = &ex->ex_phy[phy_id]; int res = 0; int i; bool last = true; /* is this the last phy of the port */
if (SAS_ADDR(changed_phy->attached_sas_addr) != 0) { for (i = 0; i < ex->num_phys; i++) { struct ex_phy *phy = &ex->ex_phy[i];
if (i == phy_id) continue; if (sas_phy_addr_match(phy, changed_phy)) {
last = false; break;
}
}
res = sas_rediscover_dev(dev, phy_id, last, i);
} else
res = sas_discover_new(dev, phy_id); return res;
}
/** * sas_ex_revalidate_domain - revalidate the domain * @port_dev: port domain device. * * NOTE: this process _must_ quit (return) as soon as any connection * errors are encountered. Connection recovery is done elsewhere. * Discover process only interrogates devices in order to discover the * domain.
*/ int sas_ex_revalidate_domain(struct domain_device *port_dev)
{ int res; struct domain_device *dev = NULL;
res = sas_find_bcast_dev(port_dev, &dev); if (res == 0 && dev) { struct expander_device *ex = &dev->ex_dev; int i = 0, phy_id;
do {
phy_id = -1;
res = sas_find_bcast_phy(dev, &phy_id, i, true); if (phy_id == -1) break;
res = sas_rediscover(dev, phy_id);
i = phy_id + 1;
} while (i < ex->num_phys);
} return res;
}
int sas_find_attached_phy_id(struct expander_device *ex_dev, struct domain_device *dev)
{ struct ex_phy *phy; int phy_id;
for (phy_id = 0; phy_id < ex_dev->num_phys; phy_id++) {
phy = &ex_dev->ex_phy[phy_id]; if (sas_phy_match_dev_addr(dev, phy)) return phy_id;
}
/* no rphy means no smp target support (ie aic94xx host) */ if (!rphy) return sas_smp_host_handler(job, shost);
switch (rphy->identify.device_type) { case SAS_EDGE_EXPANDER_DEVICE: case SAS_FANOUT_EXPANDER_DEVICE: break; default:
pr_err("%s: can we send a smp request to a device?\n",
__func__); goto out;
}
dev = sas_find_dev_by_rphy(rphy); if (!dev) {
pr_err("%s: fail to find a domain_device?\n", __func__); goto out;
}
/* do we need to support multiple segments? */ if (job->request_payload.sg_cnt > 1 ||
job->reply_payload.sg_cnt > 1) {
pr_info("%s: multiple segments req %u, rsp %u\n",
__func__, job->request_payload.payload_len,
job->reply_payload.payload_len); goto out;
}
ret = smp_execute_task_sg(dev, job->request_payload.sg_list,
job->reply_payload.sg_list); if (ret >= 0) { /* bsg_job_done() requires the length received */
rcvlen = job->reply_payload.payload_len - ret;
ret = 0;
}
out:
bsg_job_done(job, ret, rcvlen);
}
Messung V0.5
¤ Dauer der Verarbeitung: 0.23 Sekunden
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.