I read that iPadOS supports driverkit, and, presumably, the same serial FTDI UARTs as macOS.
Has this been migrated to USB-C iPhones on iOS 18?
After some searching, the developer doc is not clear, and web responses are contradictory.
We are currently using it for a wired sensor option of our BlueTooth HR sensor. When it is used in wired config, the radios are turned off. This is important to some of our customers. Since Lightning MFI sensors are being discontinued with Apple killing Lightning, we would love to have an alternative for iOS.
-- Harald
SerialDriverKit
RSS for tagDevelop drivers for serial I/O devices connected to Mac.
Posts under SerialDriverKit tag
9 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
Hello @all
I'm develop a DriverKit driver extension and without entitlement checks by OS everything runs fine. But if the entitlements check is enabled in the NVRAM then I get an error due connecting my IOUserClient instance. Which entitlements are really and exactly required for my driver?
My driver contains:
one IOUserClient instance
and multiple IOUserSerial instances
The bundle identifier of the driver ist:
org.eof.tools.VSPDriver
The bundle identifier of the client app
org.eof.tools.VSPInstall
My entire source code is available on GitHub if any one want to dive deep in :)
kernel[0:5107] () [VSPDriver]: NewUserClient called.
kernel[0:5107] () [VSPDriver]: CreateUserClient: create VSP user client from Info.plist.
kernel[0:5107] () [VSPUserClient]: init called.
kernel[0:5107] () [VSPUserClient]: init finished.
kernel[0:5107] () [VSPDriver]: CreateUserClient: check VSPUserClient type.
kernel[0:5107] () [VSPDriver]: CreateUserClient: success.
kernel[0:5107] () [VSPDriver]: NewUserClient finished.
kernel[0:5107] () [VSPUserClient]: Start: called.
kernel[0:5107] () [VSPUserClient]: User client successfully started.
kernel[0:389f] DK: VSPUserClient-0x100001127:UC failed userclient-access check, needed bundle ID org.eof.tools.VSPDriver
kernel[0:389f] DK: VSPUserClient-0x100001127:UC entitlements check failed
kernel[0:5107] () [VSPUserClient]: Stop called.
kernel[0:5107] () [VSPUserClient]: User client successfully removed.
kernel[0:5107] () [VSPUserClient]: free called.
Here my drivers entitlement file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.driverkit</key>
<true/>
<key>com.apple.developer.driverkit.allow-third-party-userclients</key>
<true/>
<key>com.apple.developer.driverkit.family.serial</key>
<true/>
</dict>
</plist>
Here my drivers Info.plist file
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2025 by EoF Software Labs</string>
<key>OSBundleUsageDescription</key>
<string>Provide virtual serial port</string>
<key>com.apple.developer.driverkit</key>
<true/>
<key>com.apple.developer.driverkit.allow-any-userclient-access</key>
<true/>
<key>com.apple.developer.driverkit.communicates-with-drivers</key>
<true/>
<key>com.apple.developer.system-extension.redistributable</key>
<true/>
<key>OSBundleLibraries</key>
<dict>
<key>com.apple.iokit.IOSerialFamily</key>
<string>1.0</string>
</dict>
<key>IOKitPersonalities</key>
<dict>
<key>VSPDriver</key>
<dict>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleIdentifierKernel</key>
<string>com.apple.kpi.iokit</string>
<key>IOMatchCategory</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>IOProviderClass</key>
<string>IOUserResources</string>
<key>IOResourceMatch</key>
<string>IOKit</string>
<key>IOProbeScore</key>
<integer>0</integer>
<key>IOClass</key>
<string>IOUserService</string>
<key>IOUserClass</key>
<string>VSPDriver</string>
<key>IOUserServerName</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>UserClientProperties</key>
<dict>
<key>IOClass</key>
<string>IOUserUserClient</string>
<key>IOUserClass</key>
<string>VSPUserClient</string>
</dict>
<key>SerialPortProperties</key>
<dict>
<key>CFBundleIdentifierKernel</key>
<string>com.apple.driver.driverkit.serial</string>
<key>IOProviderClass</key>
<string>IOSerialStreamSync</string>
<key>IOClass</key>
<string>IOUserSerial</string>
<key>IOUserClass</key>
<string>VSPSerialPort</string>
<key>HiddenPort</key>
<false/>
<key>IOTTYBaseName</key>
<string>vsp</string>
<key>IOTTYSuffix</key>
<string>0</string>
</dict>
</dict>
</dict>
</dict>
</plist>
Here the entitlements of the client app
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.driverkit</key>
<true/>
<key>com.apple.developer.driverkit.allow-third-party-userclients</key>
<true/>
<key>com.apple.developer.driverkit.communicates-with-drivers</key>
<true/>
<key>com.apple.developer.shared-with-you</key>
<true/>
<key>com.apple.developer.system-extension.install</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>$(TeamIdentifierPrefix).org.eof.apps</string>
</array>
</dict>
</plist>
Here the Info.plist of the client app:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>com.apple.developer.driverkit</key>
<true/>
<key>com.apple.developer.driverkit.install</key>
<true/>
<key>com.apple.developer.system-extension.install</key>
<true/>
<key>com.apple.developer.system-extension.uninstall</key>
<true/>
<key>com.apple.developer.driverkit.userclient-access</key>
<array>
<string>VSPDriver</string>
</array>
<key>com.apple.private.driverkit.driver-access</key>
<array>
<string>VSPDriver</string>
</array>
<key>com.apple.security.temporary-exception.iokit-user-client-class</key>
<array>
<string>IOUserUserClient</string>
</array>
</dict>
</plist>
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)
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::: 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::: 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
I would like to write a driver that supports our custom USB-C connected device, which provides a serial port interface. USBSerialDriverKit looks like the solution I need. Unfortunately, without a decent sample, I'm not sure how to accomplish this. The DriverKit documentation does a good job of telling me what APIs exist but it is very light on semantic information and details about how to use all of these API elements. A function call with five unexplained parameters just is that useful to me.
Does anyone have or know of a resource that can help me figure out how to get started?
Development environment: Xcode Version 15.1 (15C65), macOS 14.2.1 (23C71)
Run-time configuration: macOS 14.2.1 (23C71)
DESCRIPTION OF PROBLEM
1.The device supports sleep and wake functionality, and sleep/wake can be achieved on both Linux and Windows.
2.Does macOS's USBSerialDriverKit support sleep and wake? If so, how can I implement it?
3.Is it necessary to modify system permissions on macOS to use a USB serial device for sleep and wake functionality?
STEPS TO REPRODUCE
I don't know how macOS performs power management for serial devices. The sleep wake function fails to pass the test on macOS
I am attempting to communicate over serial with a USB-C device and an M-Series iPad. I have proven the device to communicate as expected (baud rate, parity, etc) via a Swift app on Mac using a third party library (IOKit) that utilizes the "AppleUSBACM (v5.0.0)" driver on macOS. I am looking to recreate this communication via iPadOS and a custom DriverKit driver that provides this same interface.
There is not an example from Apple for serial communication and DriverKit but there is a couple for communicating from an app to the dext, and for other networking examples. There are also other mentions in WWDC videos but they are incomplete and do not provide the needed structure.
Communicating between a driver extension and a client app
Connecting a network driver
Bring your driver to iPad with DriverKit
System Extensions and DriverKit
My question revolves around architecture and how to set up a driver for these needs. I have gotten the examples to run and understand what is needed for entitlements and other local signing needs. But what I don't understand is if you need a basic setup similar to the "Communicating between a driver extension and a client app" where your base driver subclasses IOService and has two arms. One that subclasses IOUserclient and allows communication between the dext and your Swift app. And another arm that subclasses IOUserSerial or IOUserUSBSerial. I assume then that these two share buffers of memory set up by the base class that allows communication between the two.
I have had little luck getting IOUserUSBSerial to compile and have made more progress on IOUserSerial. But when running that and with the supposed idVendor plist entry I am not getting that part of the dext to start or recognize when the USB device is plugged in.
Long story short, I'm looking for a basic architecture or example reference to explain serial communication in DriverKit.
Devices:
Custom USB-C hardware that is CDC ACM compliant
iPad Air 5th gen with M1 chip (iPadOS 17.2)
M1 MBP (macOS 14.2.1)
Hello,
We have a device that acts as a ***** device and communicates with a master over data sent over a USB serial port over a wired connection.
We are trying to develop an Application on iOS that will enable us to use the USB connection between iPhone and our device.
Kindly suggest to us what is the way to achieve this.
A few searches let us know that Apple's MFi program needs to be enrolled. Please let us know how this works and where could we find process for the same.
I thank you in advance!
Makarand
Hi @eskimo! With the introduction of USB-C ports on the newest iPhones, will there be any support for DriverKit (or USBSerialDriverKit 🤞) similar to the USB-C iPads?
We are wanting to enable serial communication with a custom USB peripheral via the new USB-C port. Is this (or will this) be possible in any manor?
Additionally, will this require MFi certification?
Thanks!
I'm trying to implement a virtual serial port driver for my ham radio projects which require emulating some serial port devices and I need to have a "backend" to translate the commands received by the virtual serial port into some network-based communications. I think the best way to do that is to subclass IOUserSerial? Based on the available docs on this class (https://developer.apple.com/documentation/serialdriverkit/iouserserial), I've done the basic implementation below. When the driver gets loaded, I can see sth like tty.serial-1000008DD in /dev and I can use picocom to do I/O on the virtual serial port. And I see TxDataAvailable() gets called every time I type a character in picocom.
The problems are however, firstly, when TxDataAvailable() is called, the TX buffer is all-zero so although the driver knows there is some incoming data received from picocom, it cannot actually see the data in neither Tx/Rx buffers.
Secondly, I couldn't figure out how to notify the system that there are data available for sending back to picocom. I call RxDataAvailable(), but nothing appears on picocom, and RxFreeSpaceAvailable() never gets called back. So I think I must be doing something wrong somewhere. Really appreciate it if anyone could point out how should I fix it, many thanks!
VirtualSerialPortDriver.cpp:
constexpr int bufferSize = 2048;
using SerialPortInterface = driverkit::serial::SerialPortInterface;
struct VirtualSerialPortDriver_IVars
{
IOBufferMemoryDescriptor *ifmd, *rxq, *txq;
SerialPortInterface *interface;
uint64_t rx_buf, tx_buf;
bool dtr, rts;
};
bool VirtualSerialPortDriver::init()
{
bool result = false;
result = super::init();
if (result != true)
{
goto Exit;
}
ivars = IONewZero(VirtualSerialPortDriver_IVars, 1);
if (ivars == nullptr)
{
goto Exit;
}
kern_return_t ret;
ret = ivars->rxq->Create(kIOMemoryDirectionInOut, bufferSize, 0, &ivars->rxq);
if (ret != kIOReturnSuccess) {
goto Exit;
}
ret = ivars->txq->Create(kIOMemoryDirectionInOut, bufferSize, 0, &ivars->txq);
if (ret != kIOReturnSuccess) {
goto Exit;
}
IOAddressSegment ioaddrseg;
ivars->rxq->GetAddressRange(&ioaddrseg);
ivars->rx_buf = ioaddrseg.address;
ivars->txq->GetAddressRange(&ioaddrseg);
ivars->tx_buf = ioaddrseg.address;
return true;
Exit:
return false;
}
kern_return_t
IMPL(VirtualSerialPortDriver, HwActivate)
{
kern_return_t ret;
ret = HwActivate(SUPERDISPATCH);
if (ret != kIOReturnSuccess) {
goto Exit;
}
// Loopback, set CTS to RTS, set DSR and DCD to DTR
ret = SetModemStatus(ivars->rts, ivars->dtr, false, ivars->dtr);
if (ret != kIOReturnSuccess) {
goto Exit;
}
Exit:
return ret;
}
kern_return_t
IMPL(VirtualSerialPortDriver, HwDeactivate)
{
kern_return_t ret;
ret = HwDeactivate(SUPERDISPATCH);
if (ret != kIOReturnSuccess) {
goto Exit;
}
Exit:
return ret;
}
kern_return_t
IMPL(VirtualSerialPortDriver, Start)
{
kern_return_t ret;
ret = Start(provider, SUPERDISPATCH);
if (ret != kIOReturnSuccess) {
return ret;
}
IOMemoryDescriptor *rxq_, *txq_;
ret = ConnectQueues(&ivars->ifmd, &rxq_, &txq_, ivars->rxq, ivars->txq, 0, 0, 11, 11);
if (ret != kIOReturnSuccess) {
return ret;
}
IOAddressSegment ioaddrseg;
ivars->ifmd->GetAddressRange(&ioaddrseg);
ivars->interface = reinterpret_cast<SerialPortInterface*>(ioaddrseg.address);
SerialPortInterface &intf = *ivars->interface;
ret = RegisterService();
if (ret != kIOReturnSuccess) {
goto Exit;
}
TxFreeSpaceAvailable();
Exit:
return ret;
}
void
IMPL(VirtualSerialPortDriver, TxDataAvailable)
{
SerialPortInterface &intf = *ivars->interface;
// Loopback
// FIXME consider wrapped case
size_t tx_buf_sz = intf.txPI - intf.txCI;
void *src = reinterpret_cast<void *>(ivars->tx_buf + intf.txCI);
// char src[] = "Hello, World!";
void *dest = reinterpret_cast<void *>(ivars->rx_buf + intf.rxPI);
memcpy(dest, src, tx_buf_sz);
intf.rxPI += tx_buf_sz;
RxDataAvailable();
intf.txCI = intf.txPI;
TxFreeSpaceAvailable();
Log("[TX Buf]: %{public}s", reinterpret_cast<char *>(ivars->tx_buf));
Log("[RX Buf]: %{public}s", reinterpret_cast<char *>(ivars->rx_buf));
// dmesg confirms both buffers are all-zero
Log("[TX] txPI: %d, txCI: %d, rxPI: %d, rxCI: %d, txqoffset: %d, rxqoffset: %d, txlogsz: %d, rxlogsz: %d",
intf.txPI, intf.txCI, intf.rxPI, intf.rxCI, intf.txqoffset, intf.rxqoffset, intf.txqlogsz, intf.rxqlogsz);
}
void
IMPL(VirtualSerialPortDriver, RxFreeSpaceAvailable)
{
Log("RxFreeSpaceAvailable() called!");
}
kern_return_t IMPL(VirtualSerialPortDriver,HwResetFIFO){
Log("HwResetFIFO() called with tx: %d, rx: %d!", tx, rx);
kern_return_t ret = kIOReturnSuccess;
return ret;
}
kern_return_t IMPL(VirtualSerialPortDriver,HwSendBreak){
Log("HwSendBreak() called!");
kern_return_t ret = kIOReturnSuccess;
return ret;
}
kern_return_t IMPL(VirtualSerialPortDriver,HwProgramUART){
Log("HwProgramUART() called, BaudRate: %u, nD: %d, nS: %d, P: %d!", baudRate, nDataBits, nHalfStopBits, parity);
kern_return_t ret = kIOReturnSuccess;
return ret;
}
kern_return_t IMPL(VirtualSerialPortDriver,HwProgramBaudRate){
Log("HwProgramBaudRate() called, BaudRate = %d!", baudRate);
kern_return_t ret = kIOReturnSuccess;
return ret;
}
kern_return_t IMPL(VirtualSerialPortDriver,HwProgramMCR){
Log("HwProgramMCR() called, DTR: %d, RTS: %d!", dtr, rts);
ivars->dtr = dtr;
ivars->rts = rts;
kern_return_t ret = kIOReturnSuccess;
Exit:
return ret;
}
kern_return_t IMPL(VirtualSerialPortDriver, HwGetModemStatus){
*cts = ivars->rts;
*dsr = ivars->dtr;
*ri = false;
*dcd = ivars->dtr;
Log("HwGetModemStatus() called, returning CTS=%d, DSR=%d, RI=%d, DCD=%d!", *cts, *dsr, *ri, *dcd);
kern_return_t ret = kIOReturnSuccess;
return ret;
}
kern_return_t IMPL(VirtualSerialPortDriver,HwProgramLatencyTimer){
Log("HwProgramLatencyTimer() called!");
kern_return_t ret = kIOReturnSuccess;
return ret;
}
kern_return_t IMPL(VirtualSerialPortDriver,HwProgramFlowControl){
Log("HwProgramFlowControl() called! arg: %u, xon: %d, xoff: %d", arg, xon, xoff);
kern_return_t ret = kIOReturnSuccess;
Exit:
return ret;
}