/* * Module parameters
*/ staticunsignedint no_mailboxes;
module_param_named(no_mailboxes, no_mailboxes, int, 0644);
MODULE_PARM_DESC(no_mailboxes, "There is no mailbox between cores, so ignore remote proc reply after start, default is 0 (off).");
/* Flag indicating that the remote is up and running */ #define REMOTE_IS_READY BIT(0) /* Flag indicating that the host should wait for a firmware-ready response */ #define WAIT_FW_READY BIT(1) #define REMOTE_READY_WAIT_MAX_RETRIES 500
/* * This flag is set in the DSP resource table's features field to indicate * that the firmware requires the host NOT to wait for a FW_READY response.
*/ #define FEATURE_DONT_WAIT_FW_READY BIT(0)
/* att flags */ /* DSP own area */ #define ATT_OWN BIT(31) /* DSP instruction area */ #define ATT_IRAM BIT(30)
/** * struct fw_rsc_imx_dsp - i.MX DSP specific info * * @len: length of the resource entry * @magic_num: 32-bit magic number * @version: version of data structure * @features: feature flags supported by the i.MX DSP firmware * * This represents a DSP-specific resource in the firmware's * resource table, providing information on supported features.
*/ struct fw_rsc_imx_dsp {
uint32_t len;
uint32_t magic_num;
uint32_t version;
uint32_t features;
} __packed;
/* Initialize the mailboxes between cores, if exists */ staticint (*imx_dsp_rproc_mbox_init)(struct imx_dsp_rproc *priv);
/* Reset function for DSP on i.MX8MP */ staticint imx8mp_dsp_reset(struct imx_dsp_rproc *priv)
{ void __iomem *dap = ioremap_wc(IMX8M_DAP_DEBUG, IMX8M_DAP_DEBUG_SIZE); int pwrctl;
/* Put DSP into reset and stall */
pwrctl = readl(dap + IMX8M_DAP_PWRCTL);
pwrctl |= IMX8M_PWRCTL_CORERESET;
writel(pwrctl, dap + IMX8M_DAP_PWRCTL);
/* Keep reset asserted for 10 cycles */
usleep_range(1, 2);
reset_control_assert(priv->run_stall);
/* Take the DSP out of reset and keep stalled for FW loading */
pwrctl = readl(dap + IMX8M_DAP_PWRCTL);
pwrctl &= ~IMX8M_PWRCTL_CORERESET;
writel(pwrctl, dap + IMX8M_DAP_PWRCTL);
iounmap(dap); return 0;
}
/* Reset function for DSP on i.MX8ULP */ staticint imx8ulp_dsp_reset(struct imx_dsp_rproc *priv)
{ struct arm_smccc_res res;
/* Put DSP into reset and stall */
regmap_update_bits(priv->regmap, IMX8ULP_SIM_LPAV_REG_SYSCTRL0,
IMX8ULP_SYSCTRL0_DSP_RST, IMX8ULP_SYSCTRL0_DSP_RST);
regmap_update_bits(priv->regmap, IMX8ULP_SIM_LPAV_REG_SYSCTRL0,
IMX8ULP_SYSCTRL0_DSP_STALL,
IMX8ULP_SYSCTRL0_DSP_STALL);
/* Configure resources of DSP through TFA */
arm_smccc_smc(IMX8ULP_SIP_HIFI_XRDC, 0, 0, 0, 0, 0, 0, 0, &res);
/* Take the DSP out of reset and keep stalled for FW loading */
regmap_update_bits(priv->regmap, IMX8ULP_SIM_LPAV_REG_SYSCTRL0,
IMX8ULP_SYSCTRL0_DSP_RST, 0);
regmap_update_bits(priv->regmap, IMX8ULP_SIM_LPAV_REG_SYSCTRL0,
IMX8ULP_SYSCTRL0_DSP_DBG_RST, 0);
for (i = 0; i < REMOTE_READY_WAIT_MAX_RETRIES; i++) { if (priv->flags & REMOTE_IS_READY) return 0;
usleep_range(100, 200);
}
return -ETIMEDOUT;
}
/** * imx_dsp_rproc_handle_rsc() - Handle DSP-specific resource table entries * @rproc: remote processor instance * @rsc_type: resource type identifier * @rsc: pointer to the resource entry * @offset: offset of the resource entry * @avail: available space in the resource table * * Parse the DSP-specific resource entry and update flags accordingly. * If the WAIT_FW_READY feature is set, the host must wait for the firmware * to signal readiness before proceeding with execution. * * Return: RSC_HANDLED if processed successfully, RSC_IGNORED otherwise.
*/ staticint imx_dsp_rproc_handle_rsc(struct rproc *rproc, u32 rsc_type, void *rsc, int offset, int avail)
{ struct imx_dsp_rproc *priv = rproc->priv; struct fw_rsc_imx_dsp *imx_dsp_rsc = rsc; struct device *dev = rproc->dev.parent;
if (!imx_dsp_rsc) {
dev_dbg(dev, "Invalid fw_rsc_imx_dsp.\n"); return RSC_IGNORED;
}
/* Make sure resource isn't truncated */ if (sizeof(struct fw_rsc_imx_dsp) > avail || sizeof(struct fw_rsc_imx_dsp) != imx_dsp_rsc->len) {
dev_dbg(dev, "Resource fw_rsc_imx_dsp is truncated.\n"); return RSC_IGNORED;
}
/* * If FW_RSC_NXP_S_MAGIC number is not found then * wait for fw_ready reply (default work flow)
*/ if (imx_dsp_rsc->magic_num != FW_RSC_NXP_S_MAGIC) {
dev_dbg(dev, "Invalid resource table magic number.\n"); return RSC_IGNORED;
}
/* * For now, in struct fw_rsc_imx_dsp, version 0, * only FEATURE_DONT_WAIT_FW_READY is valid. * * When adding new features, please upgrade version.
*/ if (imx_dsp_rsc->version > 0) {
dev_warn(dev, "Unexpected fw_rsc_imx_dsp version %d.\n",
imx_dsp_rsc->version); return RSC_IGNORED;
}
if (imx_dsp_rsc->features & FEATURE_DONT_WAIT_FW_READY)
priv->flags &= ~WAIT_FW_READY;
return RSC_HANDLED;
}
/* * Start function for rproc_ops * * There is a handshake for start procedure: when DSP starts, it * will send a doorbell message to this driver, then the * REMOTE_IS_READY flags is set, then driver will kick * a message to DSP.
*/ staticint imx_dsp_rproc_start(struct rproc *rproc)
{ struct imx_dsp_rproc *priv = rproc->priv; conststruct imx_dsp_rproc_dcfg *dsp_dcfg = priv->dsp_dcfg; conststruct imx_rproc_dcfg *dcfg = dsp_dcfg->dcfg; struct device *dev = rproc->dev.parent; int ret;
switch (dcfg->method) { case IMX_RPROC_MMIO:
ret = regmap_update_bits(priv->regmap,
dcfg->src_reg,
dcfg->src_mask,
dcfg->src_start); break; case IMX_RPROC_SCU_API:
ret = imx_sc_pm_cpu_start(priv->ipc_handle,
IMX_SC_R_DSP, true,
rproc->bootaddr); break; case IMX_RPROC_RESET_CONTROLLER:
ret = reset_control_deassert(priv->run_stall); break; default: return -EOPNOTSUPP;
}
if (ret)
dev_err(dev, "Failed to enable remote core!\n"); elseif (priv->flags & WAIT_FW_READY) return imx_dsp_rproc_ready(rproc);
return ret;
}
/* * Stop function for rproc_ops * It clears the REMOTE_IS_READY flags
*/ staticint imx_dsp_rproc_stop(struct rproc *rproc)
{ struct imx_dsp_rproc *priv = rproc->priv; conststruct imx_dsp_rproc_dcfg *dsp_dcfg = priv->dsp_dcfg; conststruct imx_rproc_dcfg *dcfg = dsp_dcfg->dcfg; struct device *dev = rproc->dev.parent; int ret = 0;
switch (dcfg->method) { case IMX_RPROC_MMIO:
ret = regmap_update_bits(priv->regmap, dcfg->src_reg, dcfg->src_mask,
dcfg->src_stop); break; case IMX_RPROC_SCU_API:
ret = imx_sc_pm_cpu_start(priv->ipc_handle,
IMX_SC_R_DSP, false,
rproc->bootaddr); break; case IMX_RPROC_RESET_CONTROLLER:
ret = reset_control_assert(priv->run_stall); break; default: return -EOPNOTSUPP;
}
if (ret)
dev_err(dev, "Failed to stop remote core\n"); else
priv->flags &= ~REMOTE_IS_READY;
return ret;
}
/** * imx_dsp_rproc_sys_to_da() - internal memory translation helper * @priv: private data pointer * @sys: system address (DDR address) * @len: length of the memory buffer * @da: device address to translate * * Convert system address (DDR address) to device address (DSP) * for there may be memory remap for device.
*/ staticint imx_dsp_rproc_sys_to_da(struct imx_dsp_rproc *priv, u64 sys,
size_t len, u64 *da)
{ conststruct imx_dsp_rproc_dcfg *dsp_dcfg = priv->dsp_dcfg; conststruct imx_rproc_dcfg *dcfg = dsp_dcfg->dcfg; int i;
/* Parse address translation table */ for (i = 0; i < dcfg->att_size; i++) { conststruct imx_rproc_att *att = &dcfg->att[i];
if (sys >= att->sa && sys + len <= att->sa + att->size) { unsignedint offset = sys - att->sa;
*da = att->da + offset; return 0;
}
}
return -ENOENT;
}
/* Main virtqueue message work function * * This function is executed upon scheduling of the i.MX DSP remoteproc * driver's workqueue. The workqueue is scheduled by the mailbox rx * handler. * * This work function processes both the Tx and Rx virtqueue indices on * every invocation. The rproc_vq_interrupt function can detect if there * are new unprocessed messages or not (returns IRQ_NONE vs IRQ_HANDLED), * but there is no need to check for these return values. The index 0 * triggering will process all pending Rx buffers, and the index 1 triggering * will process all newly available Tx buffers and will wakeup any potentially * blocked senders. * * NOTE: * The current logic is based on an inherent design assumption of supporting * only 2 vrings, but this can be changed if needed.
*/ staticvoid imx_dsp_rproc_vq_work(struct work_struct *work)
{ struct imx_dsp_rproc *priv = container_of(work, struct imx_dsp_rproc,
rproc_work); struct rproc *rproc = priv->rproc;
mutex_lock(&rproc->lock);
if (rproc->state != RPROC_RUNNING) goto unlock_mutex;
/* * RX door bell is used to receive the ready signal from remote * after firmware loaded.
*/
priv->rxdb_ch = mbox_request_channel_byname(cl, "rxdb"); if (IS_ERR(priv->rxdb_ch)) {
ret = PTR_ERR(priv->rxdb_ch);
dev_dbg(cl->dev, "failed to request mbox chan rxdb, ret %d\n",
ret); goto free_channel_rx;
}
/** * imx_dsp_rproc_add_carveout() - request mailbox channels * @priv: private data pointer * * This function registers specified memory entry in @rproc carveouts list * The carveouts can help to mapping the memory address for DSP.
*/ staticint imx_dsp_rproc_add_carveout(struct imx_dsp_rproc *priv)
{ conststruct imx_dsp_rproc_dcfg *dsp_dcfg = priv->dsp_dcfg; conststruct imx_rproc_dcfg *dcfg = dsp_dcfg->dcfg; struct rproc *rproc = priv->rproc; struct device *dev = rproc->dev.parent; struct device_node *np = dev->of_node; struct of_phandle_iterator it; struct rproc_mem_entry *mem; struct reserved_mem *rmem; void __iomem *cpu_addr; int a;
u64 da;
/* Remap required addresses */ for (a = 0; a < dcfg->att_size; a++) { conststruct imx_rproc_att *att = &dcfg->att[a];
if (!(att->flags & ATT_OWN)) continue;
if (imx_dsp_rproc_sys_to_da(priv, att->sa, att->size, &da)) return -EINVAL;
cpu_addr = devm_ioremap_wc(dev, att->sa, att->size); if (!cpu_addr) {
dev_err(dev, "failed to map memory %p\n", &att->sa); return -ENOMEM;
}
/* Register memory region */
mem = rproc_mem_entry_init(dev, (void __force *)cpu_addr, (dma_addr_t)att->sa,
att->size, da, NULL, NULL, "dsp_mem");
if (mem)
rproc_coredump_add_segment(rproc, da, att->size); else return -ENOMEM;
rproc_add_carveout(rproc, mem);
}
of_phandle_iterator_init(&it, np, "memory-region", NULL, 0); while (of_phandle_iterator_next(&it) == 0) { /* * Ignore the first memory region which will be used vdev buffer. * No need to do extra handlings, rproc_add_virtio_dev will handle it.
*/ if (!strcmp(it.node->name, "vdev0buffer")) continue;
rmem = of_reserved_mem_lookup(it.node); if (!rmem) {
of_node_put(it.node);
dev_err(dev, "unable to acquire memory-region\n"); return -EINVAL;
}
if (imx_dsp_rproc_sys_to_da(priv, rmem->base, rmem->size, &da)) {
of_node_put(it.node); return -EINVAL;
}
cpu_addr = devm_ioremap_wc(dev, rmem->base, rmem->size); if (!cpu_addr) {
of_node_put(it.node);
dev_err(dev, "failed to map memory %p\n", &rmem->base); return -ENOMEM;
}
/* Register memory region */
mem = rproc_mem_entry_init(dev, (void __force *)cpu_addr, (dma_addr_t)rmem->base,
rmem->size, da, NULL, NULL, it.node->name);
if (mem) {
rproc_coredump_add_segment(rproc, da, rmem->size);
} else {
of_node_put(it.node); return -ENOMEM;
}
rproc_add_carveout(rproc, mem);
}
return 0;
}
/* Prepare function for rproc_ops */ staticint imx_dsp_rproc_prepare(struct rproc *rproc)
{ struct imx_dsp_rproc *priv = rproc->priv; struct device *dev = rproc->dev.parent; struct rproc_mem_entry *carveout; int ret;
ret = imx_dsp_rproc_add_carveout(priv); if (ret) {
dev_err(dev, "failed on imx_dsp_rproc_add_carveout\n"); return ret;
}
pm_runtime_get_sync(dev);
/* * Clear buffers after pm rumtime for internal ocram is not * accessible if power and clock are not enabled.
*/
list_for_each_entry(carveout, &rproc->carveouts, node) { if (carveout->va)
memset(carveout->va, 0, carveout->len);
}
return 0;
}
/* Unprepare function for rproc_ops */ staticint imx_dsp_rproc_unprepare(struct rproc *rproc)
{
pm_runtime_put_sync(rproc->dev.parent);
return 0;
}
/* Kick function for rproc_ops */ staticvoid imx_dsp_rproc_kick(struct rproc *rproc, int vqid)
{ struct imx_dsp_rproc *priv = rproc->priv; struct device *dev = rproc->dev.parent; int err;
__u32 mmsg;
if (!priv->tx_ch) {
dev_err(dev, "No initialized mbox tx channel\n"); return;
}
/* * Send the index of the triggered virtqueue as the mu payload. * Let remote processor know which virtqueue is used.
*/
mmsg = vqid;
/* * Custom memory copy implementation for i.MX DSP Cores * * The IRAM is part of the HiFi DSP. * According to hw specs only 32-bits writes are allowed.
*/ staticint imx_dsp_rproc_memcpy(void *dst, constvoid *src, size_t size)
{ void __iomem *dest = (void __iomem *)dst; const u8 *src_byte = src; const u32 *source = src;
u32 affected_mask; int i, q, r;
u32 tmp;
/* destination must be 32bit aligned */ if (!IS_ALIGNED((uintptr_t)dest, 4)) return -EINVAL;
q = size / 4;
r = size % 4;
/* copy data in units of 32 bits at a time */ for (i = 0; i < q; i++)
writel(source[i], dest + i * 4);
if (r) {
affected_mask = GENMASK(8 * r, 0);
/* * first read the 32bit data of dest, then change affected * bytes, and write back to dest. * For unaffected bytes, it should not be changed
*/
tmp = readl(dest + q * 4);
tmp &= ~affected_mask;
/* avoid reading after end of source */ for (i = 0; i < r; i++)
tmp |= (src_byte[q * 4 + i] << (8 * i));
writel(tmp, dest + q * 4);
}
return 0;
}
/* * Custom memset implementation for i.MX DSP Cores * * The IRAM is part of the HiFi DSP. * According to hw specs only 32-bits writes are allowed.
*/ staticint imx_dsp_rproc_memset(void *addr, u8 value, size_t size)
{ void __iomem *tmp_dst = (void __iomem *)addr;
u32 tmp_val = value;
u32 affected_mask; int q, r;
u32 tmp;
/* destination must be 32bit aligned */ if (!IS_ALIGNED((uintptr_t)addr, 4)) return -EINVAL;
/* * first read the 32bit data of addr, then change affected * bytes, and write back to addr. * For unaffected bytes, it should not be changed
*/
tmp = readl(tmp_dst);
tmp &= ~affected_mask;
/* * imx_dsp_rproc_elf_load_segments() - load firmware segments to memory * @rproc: remote processor which will be booted using these fw segments * @fw: the ELF firmware image * * This function loads the firmware segments to memory, where the remote * processor expects them. * * Return: 0 on success and an appropriate error code otherwise
*/ staticint imx_dsp_rproc_elf_load_segments(struct rproc *rproc, conststruct firmware *fw)
{ struct device *dev = &rproc->dev; constvoid *ehdr, *phdr; int i, ret = 0;
u16 phnum; const u8 *elf_data = fw->data;
u8 class = fw_elf_get_class(fw);
u32 elf_phdr_get_size = elf_size_of_phdr(class);
/* go through the available ELF segments */ for (i = 0; i < phnum; i++, phdr += elf_phdr_get_size) {
u64 da = elf_phdr_get_p_paddr(class, phdr);
u64 memsz = elf_phdr_get_p_memsz(class, phdr);
u64 filesz = elf_phdr_get_p_filesz(class, phdr);
u64 offset = elf_phdr_get_p_offset(class, phdr);
u32 type = elf_phdr_get_p_type(class, phdr); void *ptr;
if (type != PT_LOAD || !memsz) continue;
dev_dbg(dev, "phdr: type %d da 0x%llx memsz 0x%llx filesz 0x%llx\n",
type, da, memsz, filesz);
if (filesz > memsz) {
dev_err(dev, "bad phdr filesz 0x%llx memsz 0x%llx\n",
filesz, memsz);
ret = -EINVAL; break;
}
if (offset + filesz > fw->size) {
dev_err(dev, "truncated fw: need 0x%llx avail 0x%zx\n",
offset + filesz, fw->size);
ret = -EINVAL; break;
}
if (!rproc_u64_fit_in_size_t(memsz)) {
dev_err(dev, "size (%llx) does not fit in size_t type\n",
memsz);
ret = -EOVERFLOW; break;
}
/* grab the kernel address for this device address */
ptr = rproc_da_to_va(rproc, da, memsz, NULL); if (!ptr) {
dev_err(dev, "bad phdr da 0x%llx mem 0x%llx\n", da,
memsz);
ret = -EINVAL; break;
}
/* put the segment where the remote processor expects it */ if (filesz) {
ret = imx_dsp_rproc_memcpy(ptr, elf_data + offset, filesz); if (ret) {
dev_err(dev, "memory copy failed for da 0x%llx memsz 0x%llx\n",
da, memsz); break;
}
}
/* zero out remaining memory for this segment */ if (memsz > filesz) {
ret = imx_dsp_rproc_memset(ptr + filesz, 0, memsz - filesz); if (ret) {
dev_err(dev, "memset failed for da 0x%llx memsz 0x%llx\n",
da, memsz); break;
}
}
}
return ret;
}
staticint imx_dsp_rproc_parse_fw(struct rproc *rproc, conststruct firmware *fw)
{ if (rproc_elf_load_rsc_table(rproc, fw))
dev_warn(&rproc->dev, "no resource table found for this firmware\n");
/** * imx_dsp_attach_pm_domains() - attach the power domains * @priv: private data pointer * * On i.MX8QM and i.MX8QXP there is multiple power domains * required, so need to link them.
*/ staticint imx_dsp_attach_pm_domains(struct imx_dsp_rproc *priv)
{ struct device *dev = priv->rproc->dev.parent; int ret;
/* A single PM domain is already attached. */ if (dev->pm_domain) return 0;
ret = dev_pm_domain_attach_list(dev, NULL, &priv->pd_list); return ret < 0 ? ret : 0;
}
/** * imx_dsp_rproc_detect_mode() - detect DSP control mode * @priv: private data pointer * * Different platform has different control method for DSP, which depends * on how the DSP is integrated in platform. * * For i.MX8QXP and i.MX8QM, DSP should be started and stopped by System * Control Unit. * For i.MX8MP and i.MX8ULP, DSP should be started and stopped by system * integration module.
*/ staticint imx_dsp_rproc_detect_mode(struct imx_dsp_rproc *priv)
{ conststruct imx_dsp_rproc_dcfg *dsp_dcfg = priv->dsp_dcfg; struct device *dev = priv->rproc->dev.parent; struct regmap *regmap; int ret = 0;
switch (dsp_dcfg->dcfg->method) { case IMX_RPROC_SCU_API:
ret = imx_scu_get_handle(&priv->ipc_handle); if (ret) return ret; break; case IMX_RPROC_MMIO:
regmap = syscon_regmap_lookup_by_phandle(dev->of_node, "fsl,dsp-ctrl"); if (IS_ERR(regmap)) {
dev_err(dev, "failed to find syscon\n"); return PTR_ERR(regmap);
}
priv->regmap = regmap; break; case IMX_RPROC_RESET_CONTROLLER:
priv->run_stall = devm_reset_control_get_exclusive(dev, "runstall"); if (IS_ERR(priv->run_stall)) {
dev_err(dev, "Failed to get DSP runstall reset control\n"); return PTR_ERR(priv->run_stall);
} break; default:
ret = -EOPNOTSUPP; break;
}
ret = imx_dsp_rproc_detect_mode(priv); if (ret) {
dev_err(dev, "failed on imx_dsp_rproc_detect_mode\n"); return ret;
}
/* There are multiple power domains required by DSP on some platform */
ret = imx_dsp_attach_pm_domains(priv); if (ret) {
dev_err(dev, "failed on imx_dsp_attach_pm_domains\n"); return ret;
} /* Get clocks */
ret = imx_dsp_rproc_clk_get(priv); if (ret) {
dev_err(dev, "failed on imx_dsp_rproc_clk_get\n"); goto err_detach_domains;
}
init_completion(&priv->pm_comp);
rproc->auto_boot = false;
ret = rproc_add(rproc); if (ret) {
dev_err(dev, "rproc_add failed\n"); goto err_detach_domains;
}
/* * There is power domain attached with mailbox, if setup mailbox * in probe(), then the power of mailbox is always enabled, * the power can't be saved. * So move setup of mailbox to runtime resume.
*/
ret = imx_dsp_rproc_mbox_init(priv); if (ret) {
dev_err(dev, "failed on imx_dsp_rproc_mbox_init\n"); return ret;
}
ret = clk_bulk_prepare_enable(DSP_RPROC_CLK_MAX, priv->clks); if (ret) {
dev_err(dev, "failed on clk_bulk_prepare_enable\n"); return ret;
}
/* Reset DSP if needed */ if (dsp_dcfg->reset)
dsp_dcfg->reset(priv);
/* Tell DSP that suspend is happening */
ret = mbox_send_message(priv->tx_ch, (void *)&mmsg); if (ret < 0) {
dev_err(dev, "PM mbox_send_message failed: %d\n", ret); return ret;
}
/* * DSP need to save the context at suspend. * Here waiting the response for DSP, then power can be disabled.
*/ if (!wait_for_completion_timeout(&priv->pm_comp, msecs_to_jiffies(100))) return -EBUSY;
out: /* * The power of DSP is disabled in suspend, so force pm runtime * to be suspend, then we can reenable the power and clocks at * resume stage.
*/ return pm_runtime_force_suspend(dev);
}
staticint imx_dsp_resume(struct device *dev)
{ struct rproc *rproc = dev_get_drvdata(dev); int ret = 0;
ret = pm_runtime_force_resume(dev); if (ret) return ret;
if (rproc->state != RPROC_RUNNING) return 0;
/* * The power of DSP is disabled at suspend, the memory of dsp * is reset, the image segments are lost. So need to reload * firmware and restart the DSP if it is in running state.
*/
ret = request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT,
rproc->firmware, dev, GFP_KERNEL,
rproc, imx_dsp_load_firmware); if (ret < 0) {
dev_err(dev, "load firmware failed: %d\n", ret); goto err;
}
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.