xref: /openbsd-src/usr.sbin/vmd/mc146818.c (revision c4fd4c5b29fc2f24970f3ce1ba4877296028afcf)
1*c4fd4c5bSdv /* $OpenBSD: mc146818.c,v 1.29 2024/07/10 09:27:33 dv Exp $ */
2ecc93de1Smlarkin /*
3ecc93de1Smlarkin  * Copyright (c) 2016 Mike Larkin <mlarkin@openbsd.org>
4ecc93de1Smlarkin  *
5ecc93de1Smlarkin  * Permission to use, copy, modify, and distribute this software for any
6ecc93de1Smlarkin  * purpose with or without fee is hereby granted, provided that the above
7ecc93de1Smlarkin  * copyright notice and this permission notice appear in all copies.
8ecc93de1Smlarkin  *
9ecc93de1Smlarkin  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10ecc93de1Smlarkin  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11ecc93de1Smlarkin  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12ecc93de1Smlarkin  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13ecc93de1Smlarkin  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14ecc93de1Smlarkin  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15ecc93de1Smlarkin  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16ecc93de1Smlarkin  */
17ecc93de1Smlarkin 
18ecc93de1Smlarkin #include <sys/types.h>
19ecc93de1Smlarkin 
20ecc93de1Smlarkin #include <dev/ic/mc146818reg.h>
21ecc93de1Smlarkin #include <dev/isa/isareg.h>
22ba66f564Sdv #include <dev/vmm/vmm.h>
23ecc93de1Smlarkin 
24ecc93de1Smlarkin #include <event.h>
25ecc93de1Smlarkin #include <stddef.h>
26ecc93de1Smlarkin #include <string.h>
27ecc93de1Smlarkin #include <time.h>
28149417b6Sreyk #include <unistd.h>
29ecc93de1Smlarkin 
30149417b6Sreyk #include "atomicio.h"
316eb4c859Sdv #include "mc146818.h"
326eb4c859Sdv #include "virtio.h"
336eb4c859Sdv #include "vmd.h"
34ecc93de1Smlarkin 
35ecc93de1Smlarkin #define MC_RATE_MASK 0xf
36ecc93de1Smlarkin 
37ecc93de1Smlarkin #define NVRAM_CENTURY 0x32
38909b2594Smlarkin #define NVRAM_MEMSIZE_LO 0x34
39909b2594Smlarkin #define NVRAM_MEMSIZE_HI 0x35
40909b2594Smlarkin #define NVRAM_HIMEMSIZE_LO 0x5B
41909b2594Smlarkin #define NVRAM_HIMEMSIZE_MID 0x5C
42909b2594Smlarkin #define NVRAM_HIMEMSIZE_HI 0x5D
43909b2594Smlarkin #define NVRAM_SMP_COUNT 0x5F
44909b2594Smlarkin 
45909b2594Smlarkin #define NVRAM_SIZE 0x60
46ecc93de1Smlarkin 
47ecc93de1Smlarkin #define TOBCD(x)	(((x) / 10 * 16) + ((x) % 10))
48ecc93de1Smlarkin 
49ecc93de1Smlarkin struct mc146818 {
50ecc93de1Smlarkin 	time_t now;
51ecc93de1Smlarkin 	uint8_t idx;
52909b2594Smlarkin 	uint8_t regs[NVRAM_SIZE];
53ecc93de1Smlarkin 	uint32_t vm_id;
54ecc93de1Smlarkin 	struct event sec;
55ecc93de1Smlarkin 	struct timeval sec_tv;
56ecc93de1Smlarkin 	struct event per;
57ecc93de1Smlarkin 	struct timeval per_tv;
58ecc93de1Smlarkin };
59ecc93de1Smlarkin 
60ecc93de1Smlarkin struct mc146818 rtc;
61ecc93de1Smlarkin 
6208fd0ce3Spd static struct vm_dev_pipe dev_pipe;
6308fd0ce3Spd 
6408fd0ce3Spd static void rtc_reschedule_per(void);
6508fd0ce3Spd 
6608fd0ce3Spd /*
6708fd0ce3Spd  * mc146818_pipe_dispatch
6808fd0ce3Spd  *
6908fd0ce3Spd  * Reads a message off the pipe, expecting a request to reschedule periodic
7008fd0ce3Spd  * interrupt rate.
7108fd0ce3Spd  */
7208fd0ce3Spd static void
mc146818_pipe_dispatch(int fd,short event,void * arg)7308fd0ce3Spd mc146818_pipe_dispatch(int fd, short event, void *arg)
7408fd0ce3Spd {
7508fd0ce3Spd 	enum pipe_msg_type msg;
7608fd0ce3Spd 
7708fd0ce3Spd 	msg = vm_pipe_recv(&dev_pipe);
7808fd0ce3Spd 	if (msg == MC146818_RESCHEDULE_PER) {
7908fd0ce3Spd 		rtc_reschedule_per();
8008fd0ce3Spd 	} else {
8108fd0ce3Spd 		fatalx("%s: unexpected pipe message %d", __func__, msg);
8208fd0ce3Spd 	}
8308fd0ce3Spd }
8408fd0ce3Spd 
85ecc93de1Smlarkin /*
86ecc93de1Smlarkin  * rtc_updateregs
87ecc93de1Smlarkin  *
88ecc93de1Smlarkin  * Updates the RTC TOD bytes, reflecting 'now'.
89ecc93de1Smlarkin  */
90ecc93de1Smlarkin static void
rtc_updateregs(void)91ecc93de1Smlarkin rtc_updateregs(void)
92ecc93de1Smlarkin {
93ecc93de1Smlarkin 	struct tm *gnow;
94ecc93de1Smlarkin 
95ecc93de1Smlarkin 	rtc.regs[MC_REGD] &= ~MC_REGD_VRT;
96ecc93de1Smlarkin 	gnow = gmtime(&rtc.now);
97ecc93de1Smlarkin 
98ecc93de1Smlarkin 	rtc.regs[MC_SEC] = TOBCD(gnow->tm_sec);
99ecc93de1Smlarkin 	rtc.regs[MC_MIN] = TOBCD(gnow->tm_min);
100ecc93de1Smlarkin 	rtc.regs[MC_HOUR] = TOBCD(gnow->tm_hour);
101ecc93de1Smlarkin 	rtc.regs[MC_DOW] = TOBCD(gnow->tm_wday + 1);
102ecc93de1Smlarkin 	rtc.regs[MC_DOM] = TOBCD(gnow->tm_mday);
103ecc93de1Smlarkin 	rtc.regs[MC_MONTH] = TOBCD(gnow->tm_mon + 1);
104ecc93de1Smlarkin 	rtc.regs[MC_YEAR] = TOBCD((gnow->tm_year + 1900) % 100);
105ecc93de1Smlarkin 	rtc.regs[NVRAM_CENTURY] = TOBCD((gnow->tm_year + 1900) / 100);
106ecc93de1Smlarkin 	rtc.regs[MC_REGD] |= MC_REGD_VRT;
107ecc93de1Smlarkin }
108ecc93de1Smlarkin 
109ecc93de1Smlarkin /*
110ecc93de1Smlarkin  * rtc_fire1
111ecc93de1Smlarkin  *
112ecc93de1Smlarkin  * Callback for the 1s periodic TOD refresh timer
113ecc93de1Smlarkin  *
114ecc93de1Smlarkin  * Parameters:
115ecc93de1Smlarkin  *  fd: unused
116ecc93de1Smlarkin  *  type: unused
117ecc93de1Smlarkin  *  arg: unused
118ecc93de1Smlarkin  */
119ecc93de1Smlarkin static void
rtc_fire1(int fd,short type,void * arg)120ecc93de1Smlarkin rtc_fire1(int fd, short type, void *arg)
121ecc93de1Smlarkin {
122e82d5294Smlarkin 	time_t old = rtc.now;
123e82d5294Smlarkin 
124e82d5294Smlarkin 	time(&rtc.now);
125e82d5294Smlarkin 
126ecc93de1Smlarkin 	rtc_updateregs();
127e82d5294Smlarkin 	if (rtc.now - old > 5) {
128e82d5294Smlarkin 		log_debug("%s: RTC clock drift (%llds), requesting guest "
129e82d5294Smlarkin 		    "resync", __func__, (rtc.now - old));
130e82d5294Smlarkin 		vmmci_ctl(VMMCI_SYNCRTC);
131e82d5294Smlarkin 	}
132ecc93de1Smlarkin 	evtimer_add(&rtc.sec, &rtc.sec_tv);
133ecc93de1Smlarkin }
134ecc93de1Smlarkin 
135ecc93de1Smlarkin /*
136ecc93de1Smlarkin  * rtc_fireper
137ecc93de1Smlarkin  *
138ecc93de1Smlarkin  * Callback for the periodic interrupt timer
139ecc93de1Smlarkin  *
140ecc93de1Smlarkin  * Parameters:
141ecc93de1Smlarkin  *  fd: unused
142ecc93de1Smlarkin  *  type: unused
143ecc93de1Smlarkin  *  arg: (as uint32_t), VM ID to which this RTC belongs
144ecc93de1Smlarkin  */
145ecc93de1Smlarkin static void
rtc_fireper(int fd,short type,void * arg)146ecc93de1Smlarkin rtc_fireper(int fd, short type, void *arg)
147ecc93de1Smlarkin {
148ecc93de1Smlarkin 	rtc.regs[MC_REGC] |= MC_REGC_PF;
149ecc93de1Smlarkin 
150*c4fd4c5bSdv 	vcpu_assert_irq((ptrdiff_t)arg, 0, 8);
151ecc93de1Smlarkin 
152ecc93de1Smlarkin 	evtimer_add(&rtc.per, &rtc.per_tv);
153ecc93de1Smlarkin }
154ecc93de1Smlarkin 
155ecc93de1Smlarkin /*
156ecc93de1Smlarkin  * mc146818_init
157ecc93de1Smlarkin  *
158ecc93de1Smlarkin  * Initializes the emulated RTC/NVRAM
159ecc93de1Smlarkin  *
160ecc93de1Smlarkin  * Parameters:
161ecc93de1Smlarkin  *  vm_id: VM ID to which this RTC belongs
162909b2594Smlarkin  *  memlo: size of memory in bytes between 16MB .. 4GB
163909b2594Smlarkin  *  memhi: size of memory in bytes after 4GB
164ecc93de1Smlarkin  */
165ecc93de1Smlarkin void
mc146818_init(uint32_t vm_id,uint64_t memlo,uint64_t memhi)166909b2594Smlarkin mc146818_init(uint32_t vm_id, uint64_t memlo, uint64_t memhi)
167ecc93de1Smlarkin {
168ecc93de1Smlarkin 	memset(&rtc, 0, sizeof(rtc));
169ecc93de1Smlarkin 	time(&rtc.now);
170ecc93de1Smlarkin 
171bfcb3828Smlarkin 	rtc.regs[MC_REGB] = MC_REGB_24HR;
172909b2594Smlarkin 
173909b2594Smlarkin 	memlo /= 65536;
174909b2594Smlarkin 	memhi /= 65536;
175909b2594Smlarkin 
176909b2594Smlarkin 	rtc.regs[NVRAM_MEMSIZE_HI] = (memlo >> 8) & 0xFF;
177909b2594Smlarkin 	rtc.regs[NVRAM_MEMSIZE_LO] = memlo & 0xFF;
178909b2594Smlarkin 	rtc.regs[NVRAM_HIMEMSIZE_HI] = (memhi >> 16) & 0xFF;
179909b2594Smlarkin 	rtc.regs[NVRAM_HIMEMSIZE_MID] = (memhi >> 8) & 0xFF;
180909b2594Smlarkin 	rtc.regs[NVRAM_HIMEMSIZE_LO] = memhi & 0xFF;
181909b2594Smlarkin 
182909b2594Smlarkin 	rtc.regs[NVRAM_SMP_COUNT] = 0;
183909b2594Smlarkin 
184ecc93de1Smlarkin 	rtc_updateregs();
185ecc93de1Smlarkin 	rtc.vm_id = vm_id;
186ecc93de1Smlarkin 
187ecc93de1Smlarkin 	timerclear(&rtc.sec_tv);
188ecc93de1Smlarkin 	rtc.sec_tv.tv_sec = 1;
189ecc93de1Smlarkin 
190c089a9adSmlarkin 	timerclear(&rtc.per_tv);
191c089a9adSmlarkin 
192ecc93de1Smlarkin 	evtimer_set(&rtc.sec, rtc_fire1, NULL);
193ecc93de1Smlarkin 	evtimer_add(&rtc.sec, &rtc.sec_tv);
194ecc93de1Smlarkin 
1959dae6e67Smlarkin 	evtimer_set(&rtc.per, rtc_fireper, (void *)(intptr_t)rtc.vm_id);
19608fd0ce3Spd 
19708fd0ce3Spd 	vm_pipe_init(&dev_pipe, mc146818_pipe_dispatch);
19808fd0ce3Spd 	event_add(&dev_pipe.read_ev, NULL);
199ecc93de1Smlarkin }
200ecc93de1Smlarkin 
201ecc93de1Smlarkin /*
2022e7a2e4aSmlarkin  * rtc_reschedule_per
203ecc93de1Smlarkin  *
204ecc93de1Smlarkin  * Reschedule the periodic interrupt firing rate, based on the currently
205ecc93de1Smlarkin  * selected REGB values.
206ecc93de1Smlarkin  */
207ecc93de1Smlarkin static void
rtc_reschedule_per(void)2082e7a2e4aSmlarkin rtc_reschedule_per(void)
209ecc93de1Smlarkin {
210ecc93de1Smlarkin 	uint16_t rate;
211ecc93de1Smlarkin 	uint64_t us;
212ecc93de1Smlarkin 
213ecc93de1Smlarkin 	if (rtc.regs[MC_REGB] & MC_REGB_PIE) {
214ecc93de1Smlarkin 		rate = 32768 >> ((rtc.regs[MC_REGA] & MC_RATE_MASK) - 1);
215ecc93de1Smlarkin 		us = (1.0 / rate) * 1000000;
216ecc93de1Smlarkin 		rtc.per_tv.tv_usec = us;
217ecc93de1Smlarkin 		if (evtimer_pending(&rtc.per, NULL))
218ecc93de1Smlarkin 			evtimer_del(&rtc.per);
219ecc93de1Smlarkin 
220ecc93de1Smlarkin 		evtimer_add(&rtc.per, &rtc.per_tv);
221ecc93de1Smlarkin 	}
222ecc93de1Smlarkin }
223ecc93de1Smlarkin 
224ecc93de1Smlarkin /*
2252e7a2e4aSmlarkin  * rtc_update_rega
226ecc93de1Smlarkin  *
227ecc93de1Smlarkin  * Updates the RTC's REGA register
228ecc93de1Smlarkin  *
229ecc93de1Smlarkin  * Parameters:
230ecc93de1Smlarkin  *  data: REGA register data
231ecc93de1Smlarkin  */
232ecc93de1Smlarkin static void
rtc_update_rega(uint32_t data)2332e7a2e4aSmlarkin rtc_update_rega(uint32_t data)
234ecc93de1Smlarkin {
235ecc93de1Smlarkin 	rtc.regs[MC_REGA] = data;
236a50a4aa6Sjasper 	if ((rtc.regs[MC_REGA] ^ data) & 0x0f)
23708fd0ce3Spd 		vm_pipe_send(&dev_pipe, MC146818_RESCHEDULE_PER);
238ecc93de1Smlarkin }
239ecc93de1Smlarkin 
240ecc93de1Smlarkin /*
2412e7a2e4aSmlarkin  * rtc_update_regb
242ecc93de1Smlarkin  *
243ecc93de1Smlarkin  * Updates the RTC's REGB register
244ecc93de1Smlarkin  *
245ecc93de1Smlarkin  * Parameters:
246ecc93de1Smlarkin  *  data: REGB register data
247ecc93de1Smlarkin  */
248ecc93de1Smlarkin static void
rtc_update_regb(uint32_t data)2492e7a2e4aSmlarkin rtc_update_regb(uint32_t data)
250ecc93de1Smlarkin {
251ecc93de1Smlarkin 	if (data & MC_REGB_DSE)
252ecc93de1Smlarkin 		log_warnx("%s: DSE mode not supported", __func__);
253ecc93de1Smlarkin 
254ecc93de1Smlarkin 	if (!(data & MC_REGB_24HR))
255ecc93de1Smlarkin 		log_warnx("%s: 12 hour mode not supported", __func__);
256ecc93de1Smlarkin 
257ecc93de1Smlarkin 	rtc.regs[MC_REGB] = data;
258ecc93de1Smlarkin 
259ecc93de1Smlarkin 	if (data & MC_REGB_PIE)
26008fd0ce3Spd 		vm_pipe_send(&dev_pipe, MC146818_RESCHEDULE_PER);
261ecc93de1Smlarkin }
262ecc93de1Smlarkin 
263ecc93de1Smlarkin /*
264ecc93de1Smlarkin  * vcpu_exit_mc146818
265ecc93de1Smlarkin  *
266ecc93de1Smlarkin  * Handles emulated MC146818 RTC access (in/out instruction to RTC ports).
267ecc93de1Smlarkin  *
268ecc93de1Smlarkin  * Parameters:
269ecc93de1Smlarkin  *  vrp: vm run parameters containing exit information for the I/O
270ecc93de1Smlarkin  *      instruction being performed
271ecc93de1Smlarkin  *
272ecc93de1Smlarkin  * Return value:
273ecc93de1Smlarkin  *  Interrupt to inject to the guest VM, or 0xFF if no interrupt should
274ecc93de1Smlarkin  *      be injected.
275ecc93de1Smlarkin  */
276ecc93de1Smlarkin uint8_t
vcpu_exit_mc146818(struct vm_run_params * vrp)277ecc93de1Smlarkin vcpu_exit_mc146818(struct vm_run_params *vrp)
278ecc93de1Smlarkin {
27902ee787fSmlarkin 	struct vm_exit *vei = vrp->vrp_exit;
280ecc93de1Smlarkin 	uint16_t port = vei->vei.vei_port;
281ecc93de1Smlarkin 	uint8_t dir = vei->vei.vei_dir;
28241b1edddSkettenis 	uint32_t data = 0;
28346635426Smlarkin 
284b966d91aSmlarkin 	get_input_data(vei, &data);
285ecc93de1Smlarkin 
286ecc93de1Smlarkin 	if (port == IO_RTC) {
287909b2594Smlarkin 		/* Discard NMI bit */
288909b2594Smlarkin 		if (data & 0x80)
289909b2594Smlarkin 			data &= ~0x80;
290909b2594Smlarkin 
291ecc93de1Smlarkin 		if (dir == 0) {
292909b2594Smlarkin 			if (data < (NVRAM_SIZE))
293ecc93de1Smlarkin 				rtc.idx = data;
29446635426Smlarkin 			else
295ecc93de1Smlarkin 				rtc.idx = MC_REGD;
29646635426Smlarkin 		} else
29746635426Smlarkin 			set_return_data(vei, rtc.idx);
298ecc93de1Smlarkin 	} else if (port == IO_RTC + 1) {
299ecc93de1Smlarkin 		if (dir == 0) {
300ecc93de1Smlarkin 			switch (rtc.idx) {
301ecc93de1Smlarkin 			case MC_SEC ... MC_YEAR:
302ecc93de1Smlarkin 			case MC_NVRAM_START ... MC_NVRAM_START + MC_NVRAM_SIZE:
303ecc93de1Smlarkin 				rtc.regs[rtc.idx] = data;
304ecc93de1Smlarkin 				break;
305ecc93de1Smlarkin 			case MC_REGA:
3062e7a2e4aSmlarkin 				rtc_update_rega(data);
307ecc93de1Smlarkin 				break;
308ecc93de1Smlarkin 			case MC_REGB:
3092e7a2e4aSmlarkin 				rtc_update_regb(data);
310ecc93de1Smlarkin 				break;
311ecc93de1Smlarkin 			case MC_REGC:
312ecc93de1Smlarkin 			case MC_REGD:
313ecc93de1Smlarkin 				log_warnx("%s: mc146818 illegal write "
314ecc93de1Smlarkin 				    "of reg 0x%x", __func__, rtc.idx);
315ecc93de1Smlarkin 				break;
316ecc93de1Smlarkin 			default:
317ecc93de1Smlarkin 				log_warnx("%s: mc146818 illegal reg %x\n",
318ecc93de1Smlarkin 				    __func__, rtc.idx);
319ecc93de1Smlarkin 			}
320ecc93de1Smlarkin 		} else {
321ecc93de1Smlarkin 			data = rtc.regs[rtc.idx];
322c089a9adSmlarkin 			set_return_data(vei, data);
323ecc93de1Smlarkin 
324ecc93de1Smlarkin 			if (rtc.idx == MC_REGC) {
325ecc93de1Smlarkin 				/* Reset IRQ state */
326ecc93de1Smlarkin 				rtc.regs[MC_REGC] &= ~MC_REGC_PF;
327ecc93de1Smlarkin 			}
328ecc93de1Smlarkin 		}
329ecc93de1Smlarkin 	} else {
330ecc93de1Smlarkin 		log_warnx("%s: mc146818 unknown port 0x%x",
331ecc93de1Smlarkin 		    __func__, vei->vei.vei_port);
332ecc93de1Smlarkin 	}
333ecc93de1Smlarkin 
334ecc93de1Smlarkin 	return 0xFF;
335ecc93de1Smlarkin }
336149417b6Sreyk 
337149417b6Sreyk int
mc146818_dump(int fd)338149417b6Sreyk mc146818_dump(int fd)
339149417b6Sreyk {
340149417b6Sreyk 	log_debug("%s: sending RTC", __func__);
341149417b6Sreyk 	if (atomicio(vwrite, fd, &rtc, sizeof(rtc)) != sizeof(rtc)) {
342149417b6Sreyk 		log_warnx("%s: error writing RTC to fd", __func__);
343149417b6Sreyk 		return (-1);
344149417b6Sreyk 	}
345149417b6Sreyk 	return (0);
346149417b6Sreyk }
347149417b6Sreyk 
348149417b6Sreyk int
mc146818_restore(int fd,uint32_t vm_id)349149417b6Sreyk mc146818_restore(int fd, uint32_t vm_id)
350149417b6Sreyk {
351149417b6Sreyk 	log_debug("%s: restoring RTC", __func__);
352149417b6Sreyk 	if (atomicio(read, fd, &rtc, sizeof(rtc)) != sizeof(rtc)) {
353149417b6Sreyk 		log_warnx("%s: error reading RTC from fd", __func__);
354149417b6Sreyk 		return (-1);
355149417b6Sreyk 	}
356149417b6Sreyk 	rtc.vm_id = vm_id;
357149417b6Sreyk 
358149417b6Sreyk 	memset(&rtc.sec, 0, sizeof(struct event));
359149417b6Sreyk 	memset(&rtc.per, 0, sizeof(struct event));
360149417b6Sreyk 	evtimer_set(&rtc.sec, rtc_fire1, NULL);
361149417b6Sreyk 	evtimer_set(&rtc.per, rtc_fireper, (void *)(intptr_t)rtc.vm_id);
362bc11ef5cSdv 
363bc11ef5cSdv 	vm_pipe_init(&dev_pipe, mc146818_pipe_dispatch);
364bc11ef5cSdv 
365149417b6Sreyk 	return (0);
366149417b6Sreyk }
367149417b6Sreyk 
368149417b6Sreyk void
mc146818_stop(void)3691999429cStb mc146818_stop(void)
370149417b6Sreyk {
371149417b6Sreyk 	evtimer_del(&rtc.per);
372149417b6Sreyk 	evtimer_del(&rtc.sec);
373bc11ef5cSdv 	event_del(&dev_pipe.read_ev);
374149417b6Sreyk }
37552e954a3Spd 
37652e954a3Spd void
mc146818_start(void)3771999429cStb mc146818_start(void)
37852e954a3Spd {
37952e954a3Spd 	evtimer_add(&rtc.sec, &rtc.sec_tv);
380bc11ef5cSdv 	event_add(&dev_pipe.read_ev, NULL);
381e605192aSpd 	rtc_reschedule_per();
38252e954a3Spd }
383