[PATCH 1/1] rust/hw/rtc: Add DS1338 RTC device implementation

3 views
Skip to first unread message

bbbbbq

unread,
Dec 6, 2025, 6:53:09 PM (10 days ago) Dec 6
to chao...@openatom.club, luo...@openatom.club, chen...@openatom.club, dz...@openatom.club, plu...@openatom.club, hust-os-ker...@googlegroups.com, bbbbbq
Implement DS1338 Real-Time Clock device in Rust as a replacement for
the C version. The DS1338 is an I2C-based RTC with 64 bytes of NVRAM.

Testing:
- Passes all qtest tests

Configuration:
- Enabled via CONFIG_X_DS1338_RUST in hw/rtc/Kconfig
- Replaces C implementation when Rust is enabled
- Added system/rtc.h to Rust bindings

Signed-off-by: bbbbbq <caoju...@openatom.com>
---
hw/rtc/Kconfig | 5 +
hw/rtc/meson.build | 2 +-
rust/Cargo.lock | 16 ++
rust/Cargo.toml | 1 +
rust/hw/Kconfig | 1 +
rust/hw/meson.build | 1 +
rust/hw/rtc/Kconfig | 4 +
rust/hw/rtc/ds1338/Cargo.toml | 31 ++++
rust/hw/rtc/ds1338/meson.build | 40 +++++
rust/hw/rtc/ds1338/src/lib.rs | 269 +++++++++++++++++++++++++++++++++
rust/hw/rtc/meson.build | 3 +
rust/system/wrapper.h | 1 +
12 files changed, 373 insertions(+), 1 deletion(-)
create mode 100644 rust/hw/rtc/Kconfig
create mode 100644 rust/hw/rtc/ds1338/Cargo.toml
create mode 100644 rust/hw/rtc/ds1338/meson.build
create mode 100644 rust/hw/rtc/ds1338/src/lib.rs
create mode 100644 rust/hw/rtc/meson.build

diff --git a/hw/rtc/Kconfig b/hw/rtc/Kconfig
index 315b0e4..140a235 100644
--- a/hw/rtc/Kconfig
+++ b/hw/rtc/Kconfig
@@ -1,8 +1,13 @@
config DS1338
bool
depends on I2C
+ select DS1338_C if !HAVE_RUST
+ select X_DS1338_RUST if HAVE_RUST
default y if I2C_DEVICES

+config DS1338_C
+ bool
+
config M41T80
bool
depends on I2C
diff --git a/hw/rtc/meson.build b/hw/rtc/meson.build
index 6c87864..fb14805 100644
--- a/hw/rtc/meson.build
+++ b/hw/rtc/meson.build
@@ -1,5 +1,5 @@

-system_ss.add(when: 'CONFIG_DS1338', if_true: files('ds1338.c'))
+system_ss.add(when: 'CONFIG_DS1338_C', if_true: files('ds1338.c'))
system_ss.add(when: 'CONFIG_M41T80', if_true: files('m41t80.c'))
system_ss.add(when: 'CONFIG_M48T59', if_true: files('m48t59.c'))
system_ss.add(when: 'CONFIG_PL031', if_true: files('pl031.c'))
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 2db2f70..8c91d88 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -91,6 +91,22 @@ dependencies = [
"qemu_macros",
]

+[[package]]
+name = "ds1388"
+version = "0.1.0"
+dependencies = [
+ "bits",
+ "bql",
+ "common",
+ "glib-sys",
+ "hwcore",
+ "migration",
+ "qom",
+ "system",
+ "trace",
+ "util",
+]
+
[[package]]
name = "either"
version = "1.12.0"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 6a17eef..66b8c3d 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -11,6 +11,7 @@ members = [
"hw/core",
"hw/char/pl011",
"hw/gpio/pcf8574",
+ "hw/rtc/ds1338",
"hw/timer/hpet",
"trace",
"util",
diff --git a/rust/hw/Kconfig b/rust/hw/Kconfig
index ba1297c..548fb20 100644
--- a/rust/hw/Kconfig
+++ b/rust/hw/Kconfig
@@ -1,4 +1,5 @@
# devices Kconfig
source char/Kconfig
source gpio/Kconfig
+source rtc/Kconfig
source timer/Kconfig
diff --git a/rust/hw/meson.build b/rust/hw/meson.build
index d6b2731..ad86738 100644
--- a/rust/hw/meson.build
+++ b/rust/hw/meson.build
@@ -1,3 +1,4 @@
subdir('char')
subdir('gpio')
+subdir('rtc')
subdir('timer')
diff --git a/rust/hw/rtc/Kconfig b/rust/hw/rtc/Kconfig
new file mode 100644
index 0000000..95081f1
--- /dev/null
+++ b/rust/hw/rtc/Kconfig
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+config X_DS1338_RUST
+ bool
diff --git a/rust/hw/rtc/ds1338/Cargo.toml b/rust/hw/rtc/ds1338/Cargo.toml
new file mode 100644
index 0000000..6f5a4ce
--- /dev/null
+++ b/rust/hw/rtc/ds1338/Cargo.toml
@@ -0,0 +1,31 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+[package]
+name = "ds1338"
+version = "0.1.0"
+authors = ["Cao Junze <caoju...@openatom.club>"]
+description = "DS1338 RTC device model for QEMU"
+resolver = "2"
+publish = false
+
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+glib-sys.workspace = true
+bits = { path = "../../../bits" }
+common = { path = "../../../common" }
+util = { path = "../../../util" }
+bql = { path = "../../../bql" }
+migration = { path = "../../../migration" }
+qom = { path = "../../../qom" }
+system = { path = "../../../system" }
+hwcore = { path = "../../../hw/core" }
+trace = { path = "../../../trace" }
+libc = { workspace = true }
+
+[lints]
+workspace = true
diff --git a/rust/hw/rtc/ds1338/meson.build b/rust/hw/rtc/ds1338/meson.build
new file mode 100644
index 0000000..501ca03
--- /dev/null
+++ b/rust/hw/rtc/ds1338/meson.build
@@ -0,0 +1,40 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# TODO: Remove this comment when the clang/libclang mismatch issue is solved.
+#
+# Rust bindings generation with `bindgen` might fail in some cases where the
+# detected `libclang` does not match the expected `clang` version/target. In
+# this case you must pass the path to `clang` and `libclang` to your build
+# command invocation using the environment variables CLANG_PATH and
+# LIBCLANG_PATH
+_libds1338_rs = static_library(
+ 'ds1338',
+ structured_sources(
+ [
+ 'src/lib.rs',
+ ],
+ ),
+ override_options: ['rust_std=2021', 'build.rust_std=2021'],
+ rust_abi: 'rust',
+ dependencies: [
+ bilge_rs,
+ bilge_impl_rs,
+ bits_rs,
+ common_rs,
+ glib_sys_rs,
+ util_rs,
+ migration_rs,
+ bql_rs,
+ qom_rs,
+ chardev_rs,
+ system_rs,
+ hwcore_rs,
+ trace_rs,
+ libc_rs,
+ ],
+)
+
+rust_devices_ss.add(when: 'CONFIG_X_DS1338_RUST', if_true: [declare_dependency(
+ link_whole: [_libds1338_rs],
+ variables: {'crate': 'ds1338'},
+)])
diff --git a/rust/hw/rtc/ds1338/src/lib.rs b/rust/hw/rtc/ds1338/src/lib.rs
new file mode 100644
index 0000000..6604b3f
--- /dev/null
+++ b/rust/hw/rtc/ds1338/src/lib.rs
@@ -0,0 +1,269 @@
+// Copyright 2025 HUST OpenAtom Open Source Club.
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+use bql::BqlRefCell;
+use hwcore::{
+ DeviceImpl, I2CEvent, I2CResult, I2CSlave, I2CSlaveClass, I2CSlaveImpl, ResetType,
+ ResettablePhasesImpl,
+};
+use migration::{
+ impl_vmstate_struct, vmstate_fields, VMStateDescription, VMStateDescriptionBuilder,
+};
+use qom::{qom_isa, ObjectImpl, ParentField};
+use std::mem::MaybeUninit;
+use system::bindings::{qemu_get_timedate, qemu_timedate_diff, tm};
+
+pub const TYPE_DS1338: &::std::ffi::CStr = c"ds1338";
+const NVRAM_SIZE: usize = 64;
+const HOURS_12: u8 = 0x40;
+const HOURS_PM: u8 = 0x20;
+const CTRL_OSF: u8 = 0x20;
+
+fn to_bcd(x: u8) -> u8 {
+ (x / 10) * 16 + (x % 10)
+}
+
+fn from_bcd(x: u8) -> u8 {
+ (x / 16) * 10 + (x % 16)
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+pub struct DS1338Inner {
+ pub offset: i64,
+ pub wday_offset: u8,
+ pub nvram: [u8; NVRAM_SIZE],
+ pub ptr: i32,
+ pub addr_byte: bool,
+}
+
+impl Default for DS1338Inner {
+ fn default() -> Self {
+ Self {
+ offset: 0,
+ wday_offset: 0,
+ nvram: [0; NVRAM_SIZE],
+ ptr: 0,
+ addr_byte: false,
+ }
+ }
+}
+
+impl_vmstate_struct!(
+ DS1338Inner,
+ VMStateDescriptionBuilder::<DS1338Inner>::new()
+ .name(c"ds1338/inner")
+ .version_id(2)
+ .minimum_version_id(1)
+ .fields(vmstate_fields! {
+ migration::vmstate_of!(DS1338Inner, offset),
+ migration::vmstate_of!(DS1338Inner, wday_offset),
+ migration::vmstate_of!(DS1338Inner, nvram),
+ migration::vmstate_of!(DS1338Inner, ptr),
+ migration::vmstate_of!(DS1338Inner, addr_byte),
+ })
+ .build()
+);
+
+#[repr(C)]
+#[derive(qom::Object, hwcore::Device)]
+pub struct DS1338State {
+ pub parent_obj: ParentField<I2CSlave>,
+ pub inner: BqlRefCell<DS1338Inner>,
+}
+
+impl DS1338Inner {
+ pub fn capture_current_time(&mut self) {
+ unsafe {
+ let mut now = MaybeUninit::<tm>::uninit();
+ qemu_get_timedate(now.as_mut_ptr(), self.offset);
+ let now = now.assume_init();
+
+ self.nvram[0] = to_bcd(now.tm_sec as u8);
+ self.nvram[1] = to_bcd(now.tm_min as u8);
+
+ if self.nvram[2] & HOURS_12 != 0 {
+ let mut tmp = now.tm_hour;
+ if tmp % 12 == 0 {
+ tmp += 12;
+ }
+ if tmp <= 12 {
+ self.nvram[2] = HOURS_12 | to_bcd(tmp as u8);
+ } else {
+ self.nvram[2] = HOURS_12 | HOURS_PM | to_bcd((tmp - 12) as u8);
+ }
+ } else {
+ self.nvram[2] = to_bcd(now.tm_hour as u8);
+ }
+
+ self.nvram[3] = ((now.tm_wday + self.wday_offset as i32) % 7 + 1) as u8;
+ self.nvram[4] = to_bcd(now.tm_mday as u8);
+ self.nvram[5] = to_bcd((now.tm_mon + 1) as u8);
+ self.nvram[6] = to_bcd((now.tm_year - 100) as u8);
+ }
+ }
+
+ pub fn inc_regptr(&mut self) {
+ self.ptr = (self.ptr + 1) & (NVRAM_SIZE as i32 - 1);
+ if self.ptr == 0 {
+ self.capture_current_time();
+ }
+ }
+
+ pub fn get_reg(&self) -> u8 {
+ self.nvram[self.ptr as usize]
+ }
+
+ pub fn set_ptr(&mut self, data: u8) {
+ self.ptr = (data & (NVRAM_SIZE as u8 - 1)) as i32;
+ }
+
+ pub fn write_time_register(&mut self, data: u8) {
+ unsafe {
+ let mut now = MaybeUninit::<tm>::uninit();
+ qemu_get_timedate(now.as_mut_ptr(), self.offset);
+ let mut now = now.assume_init();
+
+ match self.ptr {
+ 0 => {
+ now.tm_sec = from_bcd(data & 0x7f) as i32;
+ }
+ 1 => {
+ now.tm_min = from_bcd(data & 0x7f) as i32;
+ }
+ 2 => {
+ if data & HOURS_12 != 0 {
+ let mut tmp = from_bcd(data & (HOURS_PM - 1)) as i32;
+ if data & HOURS_PM != 0 {
+ tmp += 12;
+ }
+ if tmp % 12 == 0 {
+ tmp -= 12;
+ }
+ now.tm_hour = tmp;
+ } else {
+ now.tm_hour = from_bcd(data & (HOURS_12 - 1)) as i32;
+ }
+ }
+ 3 => {
+ let user_wday = ((data & 7) as i32) - 1;
+ self.wday_offset = ((user_wday - now.tm_wday + 7) % 7) as u8;
+ }
+ 4 => {
+ now.tm_mday = from_bcd(data & 0x3f) as i32;
+ }
+ 5 => {
+ now.tm_mon = from_bcd(data & 0x1f) as i32 - 1;
+ }
+ 6 => {
+ now.tm_year = from_bcd(data) as i32 + 100;
+ }
+ _ => {}
+ }
+
+ self.offset = qemu_timedate_diff(&mut now as *mut tm);
+ }
+ }
+
+ pub fn write_control_register(&mut self, data: u8) {
+ let mut data = data & 0xB3;
+ data = (data & !CTRL_OSF) | (data & self.nvram[self.ptr as usize] & CTRL_OSF);
+
+ self.nvram[self.ptr as usize] = data;
+ }
+
+ pub fn write_nvram(&mut self, data: u8) {
+ self.nvram[self.ptr as usize] = data;
+ }
+}
+
+qom_isa!(DS1338State: I2CSlave, hwcore::DeviceState, qom::Object);
+
+unsafe impl qom::ObjectType for DS1338State {
+ type Class = I2CSlaveClass;
+ const TYPE_NAME: &'static std::ffi::CStr = TYPE_DS1338;
+}
+
+impl ObjectImpl for DS1338State {
+ type ParentType = I2CSlave;
+ const CLASS_INIT: fn(&mut Self::Class) = Self::Class::class_init::<Self>;
+}
+
+impl DeviceImpl for DS1338State {
+ const VMSTATE: Option<migration::VMStateDescription<Self>> = Some(VMSTATE_DS1338);
+}
+
+impl I2CSlaveImpl for DS1338State {
+ const RECV: Option<fn(&Self) -> u8> = Some(Self::recv);
+ const SEND: Option<fn(&Self, data: u8) -> I2CResult> = Some(Self::send);
+ const EVENT: Option<fn(&Self, event: I2CEvent) -> I2CEvent> = Some(Self::event);
+}
+
+impl DS1338State {
+ fn recv(&self) -> u8 {
+ let mut inner = self.inner.borrow_mut();
+ let res = inner.get_reg();
+ inner.inc_regptr();
+ res
+ }
+
+ fn send(&self, data: u8) -> I2CResult {
+ let mut inner = self.inner.borrow_mut();
+
+ if inner.addr_byte {
+ inner.set_ptr(data);
+ inner.addr_byte = false;
+ return I2CResult::ACK;
+ }
+
+ if inner.ptr < 7 {
+ inner.write_time_register(data);
+ } else if inner.ptr == 7 {
+ inner.write_control_register(data);
+ } else {
+ inner.write_nvram(data);
+ }
+
+ inner.inc_regptr();
+ I2CResult::ACK
+ }
+
+ fn event(&self, event: I2CEvent) -> I2CEvent {
+ let mut inner = self.inner.borrow_mut();
+
+ match event {
+ I2CEvent::START_RECV => {
+ inner.capture_current_time();
+ }
+ I2CEvent::START_SEND => {
+ inner.addr_byte = true;
+ }
+ _ => {}
+ }
+
+ I2CEvent::START_RECV
+ }
+
+ fn reset_hold(&self, _reset_type: ResetType) {
+ let mut inner = self.inner.borrow_mut();
+ inner.offset = 0;
+ inner.wday_offset = 0;
+ inner.ptr = 0;
+ inner.addr_byte = false;
+ inner.nvram.fill(0);
+ }
+}
+
+impl ResettablePhasesImpl for DS1338State {
+ const HOLD: Option<fn(&Self, ResetType)> = Some(Self::reset_hold);
+}
+
+pub const VMSTATE_DS1338: VMStateDescription<DS1338State> =
+ VMStateDescriptionBuilder::<DS1338State>::new()
+ .name(c"ds1338")
+ .version_id(2)
+ .minimum_version_id(1)
+ .fields(vmstate_fields! {
+ migration::vmstate_of!(DS1338State, inner),
+ })
+ .build();
diff --git a/rust/hw/rtc/meson.build b/rust/hw/rtc/meson.build
new file mode 100644
index 0000000..8ba55f8
--- /dev/null
+++ b/rust/hw/rtc/meson.build
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+subdir('ds1338')
diff --git a/rust/system/wrapper.h b/rust/system/wrapper.h
index 48abde8..490f8f9 100644
--- a/rust/system/wrapper.h
+++ b/rust/system/wrapper.h
@@ -27,3 +27,4 @@ typedef enum memory_order {
#include "system/system.h"
#include "system/memory.h"
#include "system/address-spaces.h"
+#include "system/rtc.h"
--
2.34.1

Chao Liu

unread,
Dec 6, 2025, 7:22:57 PM (10 days ago) Dec 6
to bbbbbq, luo...@openatom.club, chen...@openatom.club, dz...@openatom.club, plu...@openatom.club, hust-os-ker...@googlegroups.com, bbbbbq
On 12/7/2025 7:52 AM, bbbbbq wrote:
> Implement DS1338 Real-Time Clock device in Rust as a replacement for
> the C version. The DS1338 is an I2C-based RTC with 64 bytes of NVRAM.
>
> Testing:
> - Passes all qtest tests
>
> Configuration:
> - Enabled via CONFIG_X_DS1338_RUST in hw/rtc/Kconfig
> - Replaces C implementation when Rust is enabled
> - Added system/rtc.h to Rust bindings
>
> Signed-off-by: bbbbbq <caoju...@openatom.com>
openatom.com -> openatom.club
Refer to PL011; this adjustment moves the source code of impl_vmstate_struct!
(along with VMSTATE_DS1338) to lower-level files.

> +#[repr(C)]
> +#[derive(qom::Object, hwcore::Device)]
> +pub struct DS1338State {
> + pub parent_obj: ParentField<I2CSlave>,
> + pub inner: BqlRefCell<DS1338Inner>,
> +}
> +
> +impl DS1338Inner {
This adjustment moves DS1338Inner to precede DS1338State.
Other LGTM :)

Miao, do you have any good reviews?

Thanks,
Chao
Reply all
Reply to author
Forward
0 new messages