DriverKit IOUserSerial Driver

Hello everyone. After a lot of research and some tests from various sources, I have actually built a small SerialDriverKit IOUserSerial driver. Unfortunately, the documentation on the official sites is tight-lipped and very thin. At least I have a running driver instance. Now my request and question: Can anyone give me a tip on how to get the data from the serial client? I have already called IOUserSerial::ConnectQueues(...) in the IOUserSerial::Start() method and I got the IOMemoryDescriptors for interrupt, RX and TX to my driver instance. I tried to get access to the memory in the method IOUserSerial::TxDataAvailable() with IOMemoryDescriptor::CreateMapping(...).

Unfortunately, no data is coming in. It's always 0x00. Here is the OS log:

kernel: (org.eof.tools.VSPDriver.dext) <private> kernel: (org.eof.tools.VSPDriver.dext) [VSPDriver] init called. kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] constructor called. kernel: (org.eof.tools.VSPDriver.dext) [VSPDriver] start called. kernel: (org.eof.tools.VSPDriver.dext) IOUserSerial::<private>: 40 0x600000da4058 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] Start called. kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] Connect INT/RX/TX buffer. kernel: (org.eof.tools.VSPDriver.dext) IOUserSerial::<private>: 59 0x600000da4058 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] prepare TCP socket. kernel: (org.eof.tools.VSPDriver.dext) [VSPDriver] driver started successfully. kernel: DK: VSPDriver-0x100000753::start(IOUserResources-0x100000116) ok ... ... some client serial setup stuff ... kernel: (IOUserSerial) IOUserSerial::hwResetFIFO: 1076 ==>0 kernel: (IOUserSerial) IOUserSerial::hwResetFIFO: 1076 <== kernel: (IOUserSerial) IOUserSerial::hwResetFIFO: 1076 locklevel = 1 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriver] HwResetFIFO called. kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] HwResetFIFO called. kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] HwResetFIFO: tx=0 rx=1 kernel: (IOUserSerial) IOUserSerial::hwResetFIFO: 1076 ==>0 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriver] TxDataAvailable called. kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable called. kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: address=0x104c22000 length=16384 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: debug TX buffer kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00 kernel: (org.eof.tools.VSPDriver.dext) [VSPDriverPrivate] TxDataAvailable: TX> 0x00

Answered by DTS Engineer in 823975022

I’m gonna ask Kevin to wade in here, because he knows the DriverKit side of this much better than I do.

I'm here...

SO, let me start with some more "foundational" guidance. Inside the DriverKit.iig file, you'll find methods declared in two different ways:

  • Standard declaration:
	virtual void            RxDataAvailable();
  • "LOCAL" declaration:
	virtual void            RxFreeSpaceAvailable() LOCAL;

The "LOCAL" keyword is critical here. As the documentation puts it:

"Tells the system that the method runs locally in the driver extension's process space."

AND:

"DriverKit adds this macro to methods that must run locally in your driver extension. A method tagged with this macro may still be called by a remote process such as the kernel. Don't add this macro to your own methods."

Unfortunately, the phrase "may still be called..." is somewhat misleading, as what we really meant was "will be called...". That is, the entire reason a method is tagged with "LOCAL" is to inform you that we need to implement it because we WILL be calling it.

That leads back to here:

Can anyone give me a tip on how to get the data from the serial client?

"ConnectQueues" is declared "LOCAL", so the system is going to be calling it, not necessarily "you". The answer for virtual terminals ends up looking something like:

kern_return_t
IMPL(<your calls name>, ConnectQueues)
{
	IOReturn err = ConnectQueues(ifmd, rxqmd, txqmd, in_rxqmd, in_txqmd,
			in_rxqoffset, in_txqoffset, in_rxqlogsz, in_txqlogsz, SUPERDISPATCH));

	assert(err == kIOReturnSuccess);

	IOAddressSegment seg;

	assert(!(*ifmd)->GetAddressRange(&seg));
	<your pointer to the control struct> = (SerialPortInterface*)seg.address;

	//track any other memory you want a reference to....
	
	return kIOReturnSuccess;
}

My question is, how can I obtain the data in the method IOUserSerial::TxAvailable which has been called by the OS

The SerialPortInterface struct that you saved a pointer to in "ConnectQueues" has the data about exactly where the serial port is in the ring buffers the other memory descriptors manage.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Here my methods:

  1. Called from Start()...

IOReturn VSPDriverPrivate::ConnectDriverQueues() { IOReturn ret;

ret = m_driver->ConnectQueues(&m_itBuffer, &m_rxBuffer, &m_txBuffer, nullptr, nullptr, 0, 0, 8, 8);
if (ret != kIOReturnSuccess) {
    VSPLog(LOG_PREFIX, "ConnectQueues failed to allocate IF/RX/TX buffers.\n");
    return ret;
}
if (m_itBuffer == nullptr) {
    VSPLog(LOG_PREFIX, "Invalid interrupt buffer detected.\n");
    ret = kIOReturnInvalid;
    return ret;
}
if (m_rxBuffer == nullptr) {
    VSPLog(LOG_PREFIX, "Invalid RX buffer detected.\n");
    return kIOReturnInvalid;
}
if (m_txBuffer == nullptr) {
    VSPLog(LOG_PREFIX, "Invalid TX buffer detected.\n");
    return kIOReturnInvalid;
}

return kIOReturnSuccess;

}

  1. Inside TxDataAvailable()

IOReturn VSPDriverPrivate::TxDataAvailable() { IOReturn ret = kIOReturnSuccess; IOMemoryMap* map = nullptr;

VSPLog(LOG_PREFIX, "TxDataAvailable called.\n");

if (m_txBuffer == nullptr) {
    return kIOReturnBadArgument;
}

// Access memory of RX IOMemoryDescriptor
ret = m_txBuffer->CreateMapping(kIOMemoryMapReadOnly, 0, 0, 0, 0, &map);
if (ret != kIOReturnSuccess) {
    VSPLog(LOG_PREFIX, "TxDataAvailable: Failed to get memory map. code=%d\n", ret);
    return ret;
}

// Send data to ...
const char* buffer = reinterpret_cast<char*>(map->GetAddress());
const uint64_t size = map->GetLength();

VSPLog(LOG_PREFIX, "TxDataAvailable: address=0x%llx length=%llu\n", (uint64_t) buffer, size);
VSPLog(LOG_PREFIX, "TxDataAvailable: debug TX buffer\n");

// !! Debug ....
for (uint64_t i = 0; i < size && i < 16; i++) {
    VSPLog(LOG_PREFIX, "TxDataAvailable: TX> 0x%02x %c\n", buffer[i], buffer[i]);
}

// copy data to send into tx FIFO buffer
memcpy(m_fifo.tx.buffer, buffer, (size < m_fifo.tx.size ? size : m_fifo.tx.size));

// !! cleanup
OSSafeReleaseNULL(map);
return kIOReturnSuccess;
Can anyone give me a tip on how to get the data from the serial client?

Either I’m confused, or you are |-:

The serial family should expose your device as a standard BSD serial device, that is, as one or more nodes in /dev. So, if you’re not doing anything weird, and you just want to use the serial port as you would any other serial port on the system, you:

  • Find the name of the /dev node in the I/O registry.

  • Interact with that using standard BSD APIs, like open and read.

The very old SerialPortSample shows the basics of this.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hello DTS Engineer, Yes, you're right, it's confusing. I forgot to mention the purpose of the extension. Sorry. The purpose of the extension is to:

  1. Use the DEXT to create nodes in '/dev' for serial ports (/dev/(tty&cu)-serail-xxxx.
  2. A user application (UI) should control the creation and linking of the serial ports.
  3. A measurement and control simulation (UI) should use the serial ports to simulate measurement and control devices 'at device level', while
  4. a second application uses the serial ports as a 'normal' interface to use the simulated measurement and control devices with different protocols .
  5. Final goal: It should be used to check whether the entire communication path works in a software framework.

In order to route the data from the user application (3) to the user application (4) and vice versa, I need to know exactly:

  1. How do I get the data from the IOMemoryDescriptor in the IOUserSerial instance?
  2. What is the mechanism for reading the incoming data reported by TxAvailable?

Thanks for the clarification.

So they way I’d approach this is to separate the data path and the control path. For the data path I’d use the serial port interface itself. You wrote:

the DEXT should do routing between the serial port /dev/tty-serial-A and /dev/tty-serial-B

Exactly that.

You can then use lower-bandwidth primitives (I/O Registry properties, simple user client messaging, and so on) for the control path.

This avoids the need for a high-bandwidth user client, getting you out of the business of memory mapping, I/O queues, and so on.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hello Quinn "The Eskimo!" ;)

thank you for you response. Okay, I'm inside the DEXT 'kernel' space. You suggestion is my doing. Let us restart to my first question context. My question is, how can I obtain the data in the method IOUserSerial::TxAvailable which has been called by the OS even if the user client app send data like: echo 'Welcome' > /dev/cu.serial-100000A5E ??

Second question ist, can I write you an email too?

[ WHERE THE DATA 'Welcome' ? ] ------------------------

2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: TxDataAvailable called.
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: TxDataAvailable: Dump m_txqmd -------------
2025-02-05 08:33:00.866 Df kernel[0:e27] (IOUserSerial) IOUserSerial::handleEventFlowControl: 1042 ==>0
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory called. md=0x600002c58d48
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory: CreateMapping.
2025-02-05 08:33:00.866 Df kernel[0:e27] (IOUserSerial) IOUserSerial::hwSendBreak: 1083 <==
2025-02-05 08:33:00.866 Df kernel[0:e27] (IOUserSerial) IOUserSerial::hwSendBreak: 1083 locklevel = 1
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory: GetAddress + GetLength.
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory: dump mapped buffer
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff=0x1025c8000 mapSize=16384
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[0]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[1]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[2]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[3]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[4]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[5]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[6]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[7]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[8]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[9]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[10]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[11]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[12]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[13]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[14]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[15]=0x00
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: TxFreeSpaceAvailable called.
2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: RxDataAvailable called.

[ SNIPPET : END ] ---------------------------------------

This is the source of the CopyMemory method which is called from IOUserSerial::TxDataAvailable implementation in my driver extension:

// --------------------------------------------------------------------
// CopyMemory_Impl(IOMemoryDescriptor* md)
// ??? Called by TxDataAvailable() and here we get always 0x00 in MD
// ??? mapped buffer of the IOMemoryDescriptors m_txqmd
//
IOReturn IMPL(VSPSerialPort, CopyMemory)
{
    IOMemoryMap* map = nullptr;
    IOReturn ret;
    
    VSPLog(LOG_PREFIX, "CopyMemory called. md=0x%llx\n", (uint64_t)md);
    
    if (md == nullptr) {
        VSPLog(LOG_PREFIX, "copy_md_memory: Invalid memory descriptor (nullptr).\n");
        return kIOReturnBadArgument;
    }
    
    VSPLog(LOG_PREFIX, "CopyMemory: CreateMapping.\n");
    
    // Access memory of TX IOMemoryDescriptor
    uint64_t mapFlags =
    kIOMemoryMapGuardedDefault |
    kIOMemoryMapCacheModeDefault |
    kIOMemoryMapReadOnly;
    ret = md->CreateMapping(mapFlags, 0, 0, 0, 0, &map);
    if (ret != kIOReturnSuccess) {
        VSPLog(LOG_PREFIX, "copy_md_memory: Failed to get memory map. code=%d\n", ret);
        return ret;
    }
    
    VSPLog(LOG_PREFIX, "CopyMemory: GetAddress + GetLength.\n");
    
    // get mapped data...
    const uint64_t mapSize = map->GetLength();
    const uint8_t* mapBuff = (uint8_t*)(map->GetAddress());
    
    VSPLog(LOG_PREFIX, "CopyMemory: dump mapped buffer\n");
    VSPLog(LOG_PREFIX, "CopyMemory MAP> mapBuff=0x%llx mapSize=%llu\n", (uint64_t) mapBuff, mapSize);
    
    // !! Debug ....
    for (uint64_t i = 0; i < mapSize && i < 16; i++) {
        VSPLog(LOG_PREFIX, "CopyMemory MAP> mapBuff[%lld]=0x%02x %c\n", i, mapBuff[i], mapBuff[i]);
    }
    
    OSSafeReleaseNULL(map);
    return kIOReturnSuccess;
}

Best Regards.

inside the DEXT 'kernel' space … how can I obtain the data in the method

I’m gonna ask Kevin to wade in here, because he knows the DriverKit side of this much better than I do.

can I write you an email too?

There’s nothing stopping you (-: However, I’m not able to provide one-on-one support via email.

You could try opening a DTS code-level support request, but I don’t think that’ll fly in this case. DTS redirects most of those to the forums these days. That way all developers can benefit from our replies.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Yes, please ask Kevin :) Thx

Accepted Answer

I’m gonna ask Kevin to wade in here, because he knows the DriverKit side of this much better than I do.

I'm here...

SO, let me start with some more "foundational" guidance. Inside the DriverKit.iig file, you'll find methods declared in two different ways:

  • Standard declaration:
	virtual void            RxDataAvailable();
  • "LOCAL" declaration:
	virtual void            RxFreeSpaceAvailable() LOCAL;

The "LOCAL" keyword is critical here. As the documentation puts it:

"Tells the system that the method runs locally in the driver extension's process space."

AND:

"DriverKit adds this macro to methods that must run locally in your driver extension. A method tagged with this macro may still be called by a remote process such as the kernel. Don't add this macro to your own methods."

Unfortunately, the phrase "may still be called..." is somewhat misleading, as what we really meant was "will be called...". That is, the entire reason a method is tagged with "LOCAL" is to inform you that we need to implement it because we WILL be calling it.

That leads back to here:

Can anyone give me a tip on how to get the data from the serial client?

"ConnectQueues" is declared "LOCAL", so the system is going to be calling it, not necessarily "you". The answer for virtual terminals ends up looking something like:

kern_return_t
IMPL(<your calls name>, ConnectQueues)
{
	IOReturn err = ConnectQueues(ifmd, rxqmd, txqmd, in_rxqmd, in_txqmd,
			in_rxqoffset, in_txqoffset, in_rxqlogsz, in_txqlogsz, SUPERDISPATCH));

	assert(err == kIOReturnSuccess);

	IOAddressSegment seg;

	assert(!(*ifmd)->GetAddressRange(&seg));
	<your pointer to the control struct> = (SerialPortInterface*)seg.address;

	//track any other memory you want a reference to....
	
	return kIOReturnSuccess;
}

My question is, how can I obtain the data in the method IOUserSerial::TxAvailable which has been called by the OS

The SerialPortInterface struct that you saved a pointer to in "ConnectQueues" has the data about exactly where the serial port is in the ring buffers the other memory descriptors manage.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hello Kevin, Thanks alot :-) Your comments gave me insight and a bright light into my darkest days :-) That helped me a lot. Also the LOCAL thing is an important aspect. A small note in context:

  1. The IOMemoryDescriptor txqmd and txqmd can be cast in IOBufferMemoryDescriptor. Perhaps the documentation should be adapted accordingly. Since it must really be assumed that the mechanism with GetAddressRange() is not available on txqmd and rxqmd (IOMemoryDescriptor does not have this method).
  1. Perhaps a small note in the documentation of the "ConnectQueues" method that is called at the beginning (presumably by open(/dev/....) or similar) by the OS. This creates more clarity for the reading developer.
  2. GetAddressRange() must be called in TxAvailable(), because in this context the buffer address of txqmd is only known and filled there.
  3. Here too, perhaps a note in the documentation of TxAvailable().

With these two notes, a different picture of how it works would emerge. I looked at apple-oss-distributions XNU on GitHub, unfortunately the SerialDriverKit was not represented there.

But, thank you for the help. I wish you all a good time. Best regards

  1. The IOMemoryDescriptor txqmd and txqmd can be cast in IOBufferMemoryDescriptor. Perhaps the documentation should be adapted accordingly. Since it must really be assumed that the mechanism with GetAddressRange() is not available on txqmd and rxqmd (IOMemoryDescriptor does not have this method).

Hmmm. That's a really weird one that I hadn't notice before. So, at a technical level, you're right that this works and that's because ConnectQueues(...) internal implementation either creates an IOBufferMemoryDescriptor from scratch (when the "in_" args are NULL) or takes the input IOMemoryDescriptor's... and then uses them to create an IOBufferMemoryDescriptor, which it returns. It's not REQUIRED to behave this way but, yes, that's what it does today.

Whether or not you rely on this is up to you. You could get the same result by creating a IOBufferMemoryDescriptor yourself from the IOMemoryDescriptor or by passing your own descriptor in. However, I would probably just OSDynamicCast to IOBufferMemoryDescriptor and then shutdown the entire DEXT if that cast fails. This is one of those cases where if it ever changed you should probably do a more detailed investigation to figure out what actually changed, instead of trying to code around it.

Perhaps a small note in the documentation of the "ConnectQueues" method that is called at the beginning (presumably by open(/dev/....) or similar) by the OS. This creates more clarity for the reading developer.

Please file a bug on this.

GetAddressRange() must be called in TxAvailable(), because in this context the buffer address of txqmd is only known and filled there.

No. The mapping here isn't dynamic- it's guaranteed to be stable for the life of the IOMemoryDescriptor. Those descriptor are destroyed in DisconnectQueues, however, you could retain them yourself if you wanted them to persist beyond them. Keep in mind that the role of IOMemoryDescriptor is to act as the object that bridge between the local address space (meaning, your processes memory), virtual memory (meaning, the system wide representation of a particular "thing"), and true physical memory (meaning, the actual address you'd push on to a PCI bus). A typical use for that descriptor would be to wire it, then pass it to a PCI card. At that point, that address is as stable as it gets.

FYI, what "GetAddressRange" actually does is call CreateMapping once (the first time it's called) to map the address, then return that address for all future calls.

Here too, perhaps a note in the documentation of TxAvailable().

Please file a bug on this.

With these two notes, a different picture of how it works would emerge. I looked at apple-oss-distributions XNU on GitHub, unfortunately the SerialDriverKit was not represented there.

I'd strongly encourage you to file bugs asking for DriverKit (and the various families) to be open sourced. I fully understand how helpful that would be and the best thing you can do to encourage that is filing bugs asking for it to happen.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Ah okay, yes I should file bugs. Well, a question about the step after receiving TX data. This - in case of you tip :) thx - is now working. I prepared already the response and the client receives the data. If the client try to transmit the next data [write(fd, p, s)] the driver does not get next data. TxAvailable isn't called any more. Finally the client runs in timeout. I'm sure that something is missing in my driver extension. DataAvailable() for client RX notification is called. Also TxFreeSpaceAvailable() is called to notify client for more data. But nothing come in again.

What is the next step or what is additional required, that the underlaying code is ready for next data?

I have RX data ready notification

RxDataAvailable Notifies the system that data from the device is now available.

I have TX ready for more data notification.

TxFreeSpaceAvailable Notifies the system that the device is ready to accept more data.

Best regards Bjoern

Ah okay, yes I should file bugs. Well, a question about the step after receiving TX data. This - in case of you tip :) thx - is now working. I prepared already the response and the client receives the data. If the client try to transmit the next data [write(fd, p, s)] the driver does not get next data. TxAvailable isn't called any more. Finally the client runs in timeout. I'm sure that something is missing in my driver extension. DataAvailable() for client RX notification is called. Also TxFreeSpaceAvailable() is called to notify client for more data.

The thing you're missing is that the primary interface that "communicates" data between the system and you is the "SerialPortInterface" structure, not the methods themselves. So, for example, "TxFreeSpaceAvailable" doesn't simply mean "I have free space available" but actually means "I have free space available AND go look at the SerialPortInterface to figure out what's happened".

You need to update those indexes as you process data so that the system understands that data is being processed so it can feed you more data.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin, okay got it.

    // Calculate available data in TX buffer
    size = ivars->m_spi->txPI - ivars->m_spi->txCI;
    
    // Get address of new TX data position
    address = ivars->m_txseg.address + ivars->m_spi->txCI;
    buffer = reinterpret_cast<uint8_t*>(address);

    /* -- do something with the buffer content -- */

    // Notify OS
    // Reset TX consumer index to end of received block
    ivars->m_spi->txCI = ivars->m_spi->txPI;

The part above calls TxAvailable() at every client transmission and increases txPI and txCI like an offset. That's for me unexpected. Here the next question:

  1. What happen, if the txPI and txCI reach the end of the TX memory descriptor? ;)
  2. Does the OS reset txPI and txCI to the beginning? -> it seems not to be.

If I report txCI = txPI and txPI = 0, the call to TxAvailable is only executed once, but in my opinion/expectation it should start again at the beginning of the TX buffer.

GitHub Project

Attached is the running/working code, but finally the DEX crash ;-)

Best Regards

Ok, the problem with the buffer overrun has been fixed :) Is a dirty? solution:

// --------------------------------------------------------------
// TxDataAvailable_Impl()
// TX data ready to read from m_txqbmd segment
void IMPL(VSPSerialPort, TxDataAvailable)
{
    IOReturn ret;
    uint8_t* buffer;
    uint64_t address;
    uint32_t size;
    bool needReset = false;

    VSPLog(LOG_PREFIX, "----------------------------------\n");
    VSPLog(LOG_PREFIX, "TxDataAvailable called.\n");
    
    // Lock to ensure thread safety
    VSPAquireLock(ivars);
    
    // Reset first
    ivars->m_txIsComplete = false;
    
    // We working...
    ivars->m_hwStatus.cts = false;
    ivars->m_hwStatus.dsr = false;
    
    // Show me indexes be fore manipulate
    VSPLog(LOG_PREFIX, "TxDataAvailable: [IOSPI-TX 1] txPI: %d, txCI: %d, txqoffset: %d, txqlogsz: %d",
           ivars->m_spi->txPI, ivars->m_spi->txCI, ivars->m_spi->txqoffset, ivars->m_spi->txqlogsz);
    
    // skip if nothing to do
    if (!ivars->m_spi->txPI) {
        VSPLog(LOG_PREFIX, "TxDataAvailable: spi->txPI is zero, skip\n");
        goto finish;
    }
 
    // Get address of new TX data position
    address = ivars->m_txseg.address + ivars->m_spi->txCI;
    buffer  = reinterpret_cast<uint8_t*>(address);

    // Calculate available data in TX buffer
    size = ivars->m_spi->txPI - ivars->m_spi->txCI;
    
#ifdef DEBUG // !! Debug ....
    VSPLog(LOG_PREFIX, "TxDataAvailable: dump buffer=0x%llx len=%u\n", (uint64_t) buffer, size);

    for (uint64_t i = 0; i < size; i++) {
        VSPLog(LOG_PREFIX, "TxDataAvailable: buffer[%02lld]=0x%02x %c\n", i, buffer[i], buffer[i]);
    }
#endif

    // Loopback TX data by async response event
    if ((ret = this->enqueueResponse(buffer, size)) != kIOReturnSuccess) {
        VSPLog(LOG_PREFIX, "TxDataAvailable: Unable to enqueue response. code=%d\n", ret);
        goto finish;
    }

    // TX -> RX echo done
    ivars->m_txIsComplete = true;

    // We reserve 1K size from the capacity from t_txqbmd. This protects
    // against a buffer overflow.
    if ((ivars->m_spi->txPI + 1024) >= (ivars->m_txseg.length - 1024)) {
        ivars->m_spi->txPI = 0;
        ivars->m_spi->txCI = 0;
    }
    // Reset TX consumer index to end of received block. This increases the
    // offset for the m_txqbmd buffer. Finally the DEXT crash if to protection.
    // Reset values like txPI = 0 and txCI = 0 after some transmissions.
    else {
        ivars->m_spi->txCI = ivars->m_spi->txPI;
    }
    
    // Show me indexes be fore manipulation
    VSPLog(LOG_PREFIX, "TxDataAvailable: [IOSPI-TX 2] txPI: %d, txCI: %d, txqoffset: %d, txqlogsz: %d",
           ivars->m_spi->txPI, ivars->m_spi->txCI, ivars->m_spi->txqoffset, ivars->m_spi->txqlogsz);

    // reset memory
    memset(buffer, 0, size);

    VSPLog(LOG_PREFIX, "TxDataAvailable complete.\n");
    
finish:
    VSPUnlock(ivars);
}

Hello Keven, the serial port driver extension executes a local echo. This works well so far. In the top instance (VSPDriver) there is a mechanism that connects two serial ports to each other. In the serial port instance (VSPSerialPort) in the method 'RxEchoAsyncEvent' it is checked whether a link exists. If so, then the TX packet should be transmitted as an echo to the other serial port as RX. According to the following pattern: Port1-TX -> Port2-RX. My problem: If I fill the Port2 RX-IOMemoryDescriptor (Buffer) with the data from Port1-TX, only one character is displayed in the terminal. No further data arrives on the terminal that is connected to Port2. Now the all-important $1 million question: What else needs to be done so that Port2 continuously sends the data to the client (e.g. Serial IO Terminal)?

The workflow Port1-TX -> TxDataAvailable() -> Enque data and dispatch async -> Same Port1 object instance gets IODataQueueDispatchSource::DataAvailable event (RxEchoAsyncEvent) -> This method then calls Port2-RX 'sendResponse' and fills the buffer of rxqmd and updates the fields in the SerialPortInterface of Port2-RX accordingly.

Best regards

DriverKit IOUserSerial Driver
 
 
Q