On Fri, 15 Aug 2025 at 10:01, Corvin Köhne <[email protected]> wrote:
>
> From: YannickV <[email protected]>
>
> This adds the Beckhoff Communication Controller (CCAT). The information
> block, EEPROM interface and DMA controller are currently implemented.
>
> The EEPROM provides production information for Beckhoff Devices.
> An EEPORM binary must therefor be handed over. It should be aligned to
> a power of two. If no EEPROM binary is handed over an empty EEPROM of
> size 4096 is initialized.
>
> This device is needed for the Beckhoff CX7200 board emulation.
>
> Signed-off-by: Yannick Voßen <[email protected]>
> ---
> hw/misc/Kconfig | 3 +
> hw/misc/beckhoff_ccat.c | 365 ++++++++++++++++++++++++++++++++++++++++
> hw/misc/meson.build | 1 +
> 3 files changed, 369 insertions(+)
> create mode 100644 hw/misc/beckhoff_ccat.c
>
> diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
> index 99548e146f..f3a2efa350 100644
> --- a/hw/misc/Kconfig
> +++ b/hw/misc/Kconfig
> @@ -223,4 +223,7 @@ config XLNX_VERSAL_TRNG
> config XLNX_ZYNQ_DDRC
> bool
>
> +config BECKHOFF_CCAT
> + bool
> +
> source macio/Kconfig
> diff --git a/hw/misc/beckhoff_ccat.c b/hw/misc/beckhoff_ccat.c
> new file mode 100644
> index 0000000000..0e21962a98
> --- /dev/null
> +++ b/hw/misc/beckhoff_ccat.c
> @@ -0,0 +1,365 @@
> +/*
> + * Beckhoff Communication Controller Emulation
> + *
> + * Copyright (c) Beckhoff Automation GmbH. & Co. KG
> + *
> + * This program is free software; you can redistribute it and/or modify it
> + * under the terms of the GNU General Public License as published by the
> + * Free Software Foundation; either version 2 of the License, or
> + * (at your option) any later version.
> + *
> + * This program is distributed in the hope that it will be useful, but
> WITHOUT
> + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
> + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
> + * for more details.
> + *
> + * You should have received a copy of the GNU General Public License along
> + * with this program; if not, see <http://www.gnu.org/licenses/>.
This needs an SPDX license identifier line. If you have that,
then you don't need the human-readable license blurb.
Is there a datasheet/manual for this device? The comment
at the top of the file is a good place to put the URL to it.
> + */
> +
> +#include "qemu/osdep.h"
> +#include "hw/sysbus.h"
> +#include "hw/register.h"
> +#include "qemu/bitops.h"
> +#include "qemu/log.h"
> +#include "qapi/error.h"
> +#include "system/block-backend.h"
> +#include "exec/address-spaces.h"
> +#include "exec/memory.h"
> +#include "system/dma.h"
> +#include "qemu/error-report.h"
> +#include "block/block.h"
> +#include "block/block_int.h"
> +#include "block/qdict.h"
> +#include "hw/block/block.h"
> +
> +#ifndef CCAT_ERR_DEBUG
> +#define CCAT_ERR_DEBUG 0
> +#endif
> +
> +#define DB_PRINT_L(level, ...) do { \
> + if (CCAT_ERR_DEBUG > (level)) { \
> + fprintf(stderr, ": %s: ", __func__); \
> + fprintf(stderr, ## __VA_ARGS__); \
> + } \
> +} while (0)
No debug printf macros in new code, please. Use either:
* tracepoints, for "this is information that might be
interesting for somebody debugging the device or code
that runs on the device"
* qemu_log_mask(LOG_UNIMP, ...), for "the guest tried
to use a feature that we don't implement"
* qemu_log_mask(LOG_GUEST_ERROR, ...), for "the guest
tried to do something that the datasheet says is
undefined behaviour/reserved/unpredictable"
> +
> +#define DB_PRINT(...) DB_PRINT_L(0, ## __VA_ARGS__)
> +
> +#define TYPE_BECKHOFF_CCAT "beckhoff-ccat"
> +#define BECKHOFF_CCAT(obj) \
> + OBJECT_CHECK(BeckhoffCcat, (obj), TYPE_BECKHOFF_CCAT)
Prefer the new-style OBJECT_DECLARE_SIMPLE_TYPE etc
macros, rather than defining the cast macro yourself.
> +
> +#define MAX_NUM_SLOTS 32
> +
> +#define CCAT_EEPROM_OFFSET 0x100
> +#define CCAT_DMA_OFFSET 0x8000
> +
> +#define CCAT_MEM_SIZE 0xFFFF
> +#define CCAT_DMA_SIZE 0x800
> +#define CCAT_EEPROM_SIZE 0x20
> +
> +#define EEPROM_MEMORY_SIZE 0x1000
> +
> +#define EEPROM_CMD_OFFSET (CCAT_EEPROM_OFFSET + 0x00)
> + #define EEPROM_CMD_WRITE_MASK 0x2
> + #define EEPROM_CMD_READ_MASK 0x1
> +#define EEPROM_ADR_OFFSET (CCAT_EEPROM_OFFSET + 0x04)
> +#define EEPROM_DATA_OFFSET (CCAT_EEPROM_OFFSET + 0x08)
> +
> +#define DMA_BUFFER_OFFSET (CCAT_DMA_OFFSET + 0x00)
> +#define DMA_DIRECTION_OFFSET (CCAT_DMA_OFFSET + 0x7c0)
> + #define DMA_DIRECTION_MASK 1
> +#define DMA_TRANSFER_OFFSET (CCAT_DMA_OFFSET + 0x7c4)
> +#define DMA_HOST_ADR_OFFSET (CCAT_DMA_OFFSET + 0x7c8)
> +#define DMA_TRANSFER_LENGTH_OFFSET (CCAT_DMA_OFFSET + 0x7cc)
> +
> +/*
> + * The informationblock is always located at address 0x0.
> + * Address and size are therefor replaced by two identifiers.
> + * The Parameter give information about the maximal number of
> + * function slots and the creation date (in this case 01.01.2001)
> + */
> +#define CCAT_ID_1 0x88a4
> +#define CCAT_ID_2 0x54414343
> +#define CCAT_INFO_BLOCK_PARAMS (MAX_NUM_SLOTS << 0) | (0x1 << 8) | \
> + (0x1 << 16) | (0x1 << 24)
> +
> +#define CCAT_FUN_TYPE_ENTRY 0x0001
> +#define CCAT_FUN_TYPE_EEPROM 0x0012
> +#define CCAT_FUN_TYPE_DMA 0x0013
> +
> +typedef struct BeckhoffCcat {
> + SysBusDevice parent_obj;
> +
> + MemoryRegion iomem;
> +
> + uint8_t mem[CCAT_MEM_SIZE];
> +
> + BlockBackend *eeprom_blk;
> + uint8_t *eeprom_storage;
> + int64_t eeprom_size;
> +} BeckhoffCcat;
> +
> +typedef struct __attribute__((packed)) CcatFunctionBlock {
> + uint16_t type;
> + uint16_t revision;
> + uint32_t parameter;
> + uint32_t address_offset;
> + uint32_t size;
> +} CcatFunctionBlock;
"attribute((packed))" is a bit of a red flag of doing something
wrong. In this case, you use this in beckhoff_ccat_reset() to
set up a host C structure that you memcpy() into s->mem[].
That is not portable to big-endian systems.
> +
> +static void sync_eeprom(BeckhoffCcat *s)
> +{
> + if (!s->eeprom_blk) {
> + return;
> + }
> + blk_pwrite(s->eeprom_blk, 0, s->eeprom_size, s->eeprom_storage, 0);
> +}
> +
> +static uint64_t beckhoff_ccat_eeprom_read(void *opaque, hwaddr addr,
> + unsigned size)
> +{
> + BeckhoffCcat *s = opaque;
> + uint64_t val = 0;
> + memcpy(&val, &s->mem[addr], size);
What is this trying to do ? If you want to read
N bytes of data in little endian order from a host
address, use ldn_le_p(). (Also available in "big endian"
and "host endianness" flavours.)
> + return val;
> +}
> +
> +static void beckhoff_ccat_eeprom_write(void *opaque, hwaddr addr, uint64_t
> val,
> + unsigned size)
> +{
> + BeckhoffCcat *s = opaque;
> + uint64_t eeprom_adr;
> + switch (addr) {
> + case EEPROM_CMD_OFFSET:
> + eeprom_adr = *(uint32_t *)&s->mem[EEPROM_ADR_OFFSET];
> + eeprom_adr = (eeprom_adr * 2) % s->eeprom_size;
> + if (val & EEPROM_CMD_READ_MASK) {
> + uint64_t buf = 0;
> + uint32_t bytes_to_read = 8;
> + if (eeprom_adr > s->eeprom_size - 8) {
> + bytes_to_read = s->eeprom_size - eeprom_adr;
> + }
> + memcpy(&buf, s->eeprom_storage + eeprom_adr, bytes_to_read);
> + *(uint64_t *)&s->mem[EEPROM_DATA_OFFSET] = buf;
All this code with casting to different pointer types and
using memcpy() looks very suspicious. Use the relevant
ld*_*_p() functions to load the right sized values at the
right endianness. (Casting from one pointer type to another
is usually an indication that you're going down a wrong path.)
> +
> + } else if (val & EEPROM_CMD_WRITE_MASK) {
> + uint32_t buf = *(uint32_t *)&s->mem[EEPROM_DATA_OFFSET];
> + memcpy(s->eeprom_storage + eeprom_adr, &buf, 2);
> + sync_eeprom(s);
> + }
> + break;
> + default:
> + memcpy(&s->mem[addr], &val, size);
> + }
> +}
> +
> +static uint64_t beckhoff_ccat_dma_read(void *opaque, hwaddr addr, unsigned
> size)
> +{
> + BeckhoffCcat *s = opaque;
> + uint64_t val = 0;
> +
> + switch (addr) {
> + case DMA_TRANSFER_OFFSET:
> + if (s->mem[DMA_TRANSFER_OFFSET] & 0x1) {
> + DB_PRINT("DMA transfer finished\n");
> + s->mem[DMA_TRANSFER_OFFSET] = 0;
> + }
> + break;
> + }
> + memcpy(&val, &s->mem[addr], size);
> + return val;
> +}
> +
> +static void beckhoff_ccat_dma_write(void *opaque, hwaddr addr, uint64_t val,
> + unsigned size)
> +{
> + BeckhoffCcat *s = opaque;
> + switch (addr) {
> + case DMA_TRANSFER_OFFSET:
> + uint8_t len = s->mem[DMA_TRANSFER_LENGTH_OFFSET];
> + uint8_t *mem_buf = &s->mem[DMA_BUFFER_OFFSET];
> +
> + if (s->mem[DMA_DIRECTION_OFFSET] & DMA_DIRECTION_MASK) {
> + dma_addr_t dmaAddr = *(uint32_t *)&s->mem[DMA_HOST_ADR_OFFSET];
Should probably be ldl_le_p().
> + dma_memory_read(&address_space_memory, dmaAddr,
> + mem_buf, len * 8, MEMTXATTRS_UNSPECIFIED);
> + } else {
> + dma_addr_t dmaAddr = *(uint32_t *)&s->mem[DMA_HOST_ADR_OFFSET];
> + dma_memory_write(&address_space_memory, dmaAddr + 8,
> + mem_buf, len * 8, MEMTXATTRS_UNSPECIFIED);
Does this really use addr for reads and addr + 8 for writes ?
> + }
> + break;
> + }
> + memcpy(&s->mem[addr], &val, size);
> +}
> +static uint64_t beckhoff_ccat_read(void *opaque, hwaddr addr, unsigned size)
> +{
> + DB_PRINT("CCAT_READ addr=0x%lx size=%u\n", addr, size);
> +
> + BeckhoffCcat *s = opaque;
> + uint64_t val = 0;
> +
> + if (addr > CCAT_MEM_SIZE - size) {
> + error_report("Overflow. Address or size is too large.\n");
> + exit(1);
Don't error_report() and exit for things like this.
Either:
(a) this is not possible unless there's a bug in QEMU:
in that case, use assert()
(b) this is possible if the guest does something silly:
in that case use qemu_log_mask(LOG_GUEST_ERROR, ...)
In this case it looks like an assert() is appropriate, because
the memory_region_init_io() size argument should ensure that
you can't get here with an addr + size that is beyond the
end of the block.
> + }
> +
> + if (addr >= CCAT_EEPROM_OFFSET &&
> + addr <= CCAT_EEPROM_OFFSET + s->eeprom_size) {
> + return beckhoff_ccat_eeprom_read(opaque, addr, size);
> + } else if (addr >= CCAT_DMA_OFFSET &&
> + addr <= CCAT_DMA_OFFSET + CCAT_DMA_SIZE) {
> + return beckhoff_ccat_dma_read(opaque, addr, size);
> + } else {
> + memcpy(&val, &s->mem[addr], size);
> + }
> +
> + return val;
> +}
> +
> +static void beckhoff_ccat_write(void *opaque, hwaddr addr, uint64_t val,
> + unsigned size)
> +{
> + DB_PRINT("CCAT_WRITE addr=0x%lx size=%u val=0x%lx\n", addr, size, val);
> +
> + BeckhoffCcat *s = opaque;
> +
> + if (addr > CCAT_MEM_SIZE - size) {
> + error_report("Overflow. Address or size is too large.\n");
> + exit(1);
> + }
> +
> + if (addr >= CCAT_EEPROM_OFFSET &&
> + addr <= CCAT_EEPROM_OFFSET + s->eeprom_size) {
> + beckhoff_ccat_eeprom_write(opaque, addr, val, size);
> + } else if (addr >= CCAT_DMA_OFFSET &&
> + addr <= CCAT_DMA_OFFSET + CCAT_DMA_SIZE) {
> + beckhoff_ccat_dma_write(opaque, addr, val, size);
> + } else {
> + memcpy(&s->mem[addr], &val, size);
> + }
> +}
> +
> +static const MemoryRegionOps beckhoff_ccat_ops = {
> + .read = beckhoff_ccat_read,
> + .write = beckhoff_ccat_write,
> + .endianness = DEVICE_LITTLE_ENDIAN,
> + .valid = {
> + .min_access_size = 1,
> + .max_access_size = 8,
> + },
> +};
> +
> +
> +static void beckhoff_ccat_reset(DeviceState *dev)
> +{
> + BeckhoffCcat *s = BECKHOFF_CCAT(dev);
> +
> + CcatFunctionBlock function_blocks[MAX_NUM_SLOTS] = {0};
> +
> + CcatFunctionBlock info_block = {
> + .type = CCAT_FUN_TYPE_ENTRY,
> + .revision = 0x0001,
> + .parameter = CCAT_INFO_BLOCK_PARAMS,
> + .address_offset = CCAT_ID_1,
> + .size = CCAT_ID_2
> + };
> + CcatFunctionBlock eeprom_block = {
> + .type = CCAT_FUN_TYPE_EEPROM,
> + .revision = 0x0001,
> + .parameter = 0,
> + .address_offset = CCAT_EEPROM_OFFSET,
> + .size = CCAT_EEPROM_SIZE
> + };
> + CcatFunctionBlock dma_block = {
> + .type = CCAT_FUN_TYPE_DMA,
> + .revision = 0x0000,
> + .parameter = 0,
> + .address_offset = CCAT_DMA_OFFSET,
> + .size = CCAT_DMA_SIZE
> + };
> +
> + /*
> + * The EEPROM interface is usually at function slot 11.
> + * The DMA controller is usually at function slot 15.
> + */
> + function_blocks[0] = info_block;
> + function_blocks[11] = eeprom_block;
> + function_blocks[15] = dma_block;
> +
> + memcpy(&s->mem[0], function_blocks, sizeof(function_blocks));
This isn't going to work on big-endian hosts, as noted above.
> +}
> +
> +static void beckhoff_ccat_realize(DeviceState *dev, Error **errp)
> +{
> + BeckhoffCcat *s = BECKHOFF_CCAT(dev);
> + BlockBackend *blk;
> +
> + blk = blk_by_name("ccat-eeprom");
No other device calls blk_by_name() to get its block backend:
you should not do so either. Use a qdev property and have
the SoC/board model set it.
> +
> + if (blk) {
> + uint64_t blk_size = blk_getlength(blk);
> + if (!is_power_of_2(blk_size)) {
> + error_report("Blockend size is not a power of two.");
In a realize function you have an Error** argument, so you
should report errors via error_setg() + return.
> + }
> +
> + if (blk_size < 512) {
> + error_report("Blockend size is too small. Using backup.");
> + s->eeprom_size = EEPROM_MEMORY_SIZE;
> + s->eeprom_storage = blk_blockalign(NULL, s->eeprom_size);
> + memset(s->eeprom_storage, 0x00, s->eeprom_size);
We don't try to fix things up like this in other block-backed
devices, I think, so we shouldn't do it here either.
> + } else {
> + DB_PRINT("EEPROM block backend found\n");
> + blk_set_perm(blk, BLK_PERM_WRITE, BLK_PERM_ALL, errp);
> +
> + s->eeprom_size = blk_size;
> + s->eeprom_blk = blk;
> + s->eeprom_storage = blk_blockalign(s->eeprom_blk,
> s->eeprom_size);
> +
> + if (!blk_check_size_and_read_all(s->eeprom_blk, DEVICE(s),
> + s->eeprom_storage,
> s->eeprom_size,
> + errp)) {
> + exit(1);
Don't silently exit(): use 'return', to report the error
to your caller.
> + }
> + }
> + } else {
> + s->eeprom_size = EEPROM_MEMORY_SIZE;
> + s->eeprom_storage = blk_blockalign(NULL, s->eeprom_size);
> + memset(s->eeprom_storage, 0x00, s->eeprom_size);
> + }
> +
> + beckhoff_ccat_reset(dev);
> +}
> +
> +static void beckhoff_ccat_init(Object *obj)
> +{
> + BeckhoffCcat *s = BECKHOFF_CCAT(obj);
> + SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
> +
> + memory_region_init_io(&s->iomem, obj, &beckhoff_ccat_ops, s,
> + TYPE_BECKHOFF_CCAT, CCAT_MEM_SIZE);
> + sysbus_init_mmio(sbd, &s->iomem);
> +}
> +
> +static void beckhoff_ccat_class_init(ObjectClass *klass, void *data)
> +{
> + DeviceClass *dc = DEVICE_CLASS(klass);
> + dc->realize = beckhoff_ccat_realize;
The guest can read and write mem[], so I think you need
a reset handler, and also a VMState for migration.
> +}
thanks
-- PMM