Custom hardware, accessing serial port from in-kernel

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



Hi!

I'm working with a DSM-G600 device, a ppc-based network-storage unit from D-Link that is extremely hackable: http://dsmg600.info/

It uses a customized version of linux-2.4.19-pre4, and is (nominally) a "sandpoint" platfrom. I'm not using the D-Link firmware; rather, I'm booting gentoo-embedded from a hard drive as described here: http://dsmg600.info/howto:hdd_boot


One of the oddities of this unit is that some of the peripheral controls -- front panel LEDs, the "off" button, etc -- are controlled and monitored by a small microcontroller (I think it's a PIC). This microcontroller is accessed from the ppc via the ppc's second serial port. So, you can monitor/control all of these functions from userspace by sending certain magic strings to /dev/ttyS1, or calling certain ioctls on it.

Except there's one wrinkle: to actually *shut off* power, you have to send the magic string "ZWC" to the ttyS1. Now, ideally you'd do this from your platform's <struct machdep_calls>.power_off() function. That way, when you do a 'shutdown' from user space, the following sequence occurs: (1) init switches to shutdown mode, shutting down all extra processes and eventually unmounting disks, remounting root as ro, etc
  (2) eventually the shutdown syscall is called, which
(3) wends its way down into the architecture's machine_shutdown(), which then calls the platform <struct machdep_calls>.power_off() function pointer. (4) which actually turns off the power -- by sending "ZWC" to the second UART.

Now, I _know_ writing "files" (or devices, like /dev/tts/1) from inside kernel space is a bad idea. But I didn't design this hardware -- so how can I access the serial port, from in-kernel, at a stage in the shutdown sequence when almost everything has already been deactivated?

The attached patch shows what I've attempted so far. Along with turning on a lot of debugging output, the patch creates a new syscall that performs the desired function from in-kernel. (I only did this so I could prove that the functionality actually works, during a "normal" -- e.g. non-startup/non-shutdown -- kernel context.)

A small test program accessing this syscall shows that THAT part, at least, does work. I can use my syscall to flash the lights and cut power (that last test is a bad idea, actually: since I'm in user space, I've got lots of running processes and mounted disks...). All that is implemented by the patches to:

ppclinux_spca5xx_le/arch/ppc/kernel/misc.S
ppclinux_spca5xx_le/arch/ppc/kernel/syscalls.c
ppclinux_spca5xx_le/arch/ppc/platforms/sandpoint_serial.h
ppclinux_spca5xx_le/drivers/char/serial.c

The syscall is implemented in two parts:
* syscalls.c:sys_setleds() which does some checking and copies the desired magic string from user to kernel space, and * syscalls.c:priv_sys_setleds() which does the actual work, and expects its input string to be in kernel memory.

The second function, hopefully, is what would also be called by the intended user of this "feature": sandpoint_setup:sandpoint_power_off().

The last bit of the patch modifies

ppclinux_spca5xx_le/arch/ppc/platforms/sandpoint_setup.c
ppclinux_spca5xx_le/arch/ppc/kernel/setup.c

to actually call priv_sys_setleds() with the magic "ZWC" string from within sandpoint_power_off(), and adds some additional debugging.

Here's what I would expect to see on the serial (ttyS0, not 1, [note A]) console if all this worked (ignore the [x] markers)

...
 * Stopping devfsd ...                               [ ok ]
 * Deactivating swap ...                             [ ok ]
 * Unmounting filesystems ...                        [ ok ]
 * Remounting remaining filesystems readonly ...     [ ok ]
[1] Power down.
[2] ppc: machine_power_off ENTER
[3] about to send ZWC cmd
[4] core send: ZWC
[5] rs_open tts/%d1, count = 1
[6] rs_open ttys1 successful... [7] rs_close ttys1, count = 0
[8] core sent: WLO
[9] sent ZWC cmd

but that somewhere along the chain, probably between [6] and [7],the microcontroller would ACTUALLY cut off power and the whole machine would go down.

[1] kernel/sys.c (sys_reboot) [LINUX_REBOOT_CMD_POWER_OFF]
[2] arch/ppc/kernel/setup.c (machine_power_off)
[3] arch/ppc/platforms/sandpoint_setup.c (sandpoint_power_off)
[4] arch/ppc/kernel/syscalls.c (priv_sys_setleds)
[5] from serial driver, because I've turned on SERIAL_DEBUG_OPEN
[6] ditto
[7] ditto
[8] arch/ppc/kernel/syscalls.c (priv_sys_setleds)
[9] arch/ppc/platforms/sandpoint_setup.c (sandpoint_power_off)
    -- which never returns, even if power *doesn't* get shut off.

However, what I actually see is:
 * Stopping devfsd ...                               [ ok ]
 * Deactivating swap ...                             [ ok ]
 * Unmounting filesystems ...                        [ ok ]
 * Remounting remaining filesystems readonly ...     [ ok ]
[1] Power down.
[2] ppc: machine_power_off ENTER
[3] about to send ZWC cmd
[4] core send: ZWC
[5] rs_open tts/%d1, count = 1

and then nothing. power stays on, but the machine is basically stopped. I don't know if it's stuck in some interrupt-related deadlock, or if it ended up back in sandpoint_power_off's infloop but without ever *actually* sending the magic string to the microcontroller, or what.

As you can see from the patch, I'm using EVIL EVIL filp_open from in-kernel to access /dev/tts/1. Maybe that part, which while dangerous does actually work when the system is up and running, is broken at this late shutdown stage?

So here's my question: what should I do differently in priv_sys_setleds() so that I can send the magic "ZWC" string to the second serial port from in-kernel, during the shutdown sequence? Is it possible to initialize a raw struct_serial without an associated tty and tty line discipline, and send the exact byte stream corresponding to "ZWC\r\n" with 8N1, 9600B thru direct bit-banging or byte-writing? Or can I directly initialize a tty_struct WITHOUT using the fs layer?

TIA,
Chuck

[note A]: the device doesn't ship with a user-accessible external serial port. You have to do some hardware hacking, as described here: http://dsmg600.info/howto:serial_connector
diff -u ppclinux/arch/ppc/kernel/misc.S ppclinux_spca5xx_le/arch/ppc/kernel/misc.S
--- ppclinux/arch/ppc/kernel/misc.S	2005-04-10 22:50:07.000000000 -0400
+++ ppclinux_spca5xx_le/arch/ppc/kernel/misc.S	2007-09-08 19:55:27.000000000 -0400
@@ -1293,6 +1293,7 @@
 	.long sys_ni_syscall 	/*	reserved for sys_io_getevents */
 	.long sys_ni_syscall 	/* 230	reserved for sys_io_submit */
 	.long sys_ni_syscall 	/*	reserved for sys_io_cancel */
+	.long sys_setleds	/* 232  */
 
 	.rept NR_syscalls-(.-sys_call_table)/4
 		.long sys_ni_syscall
diff -u ppclinux/include/asm-ppc/unistd.h ppclinux_spca5xx_le/include/asm-ppc/unistd.h
--- ppclinux/include/asm-ppc/unistd.h	2005-04-10 22:50:56.000000000 -0400
+++ ppclinux_spca5xx_le/include/asm-ppc/unistd.h	2007-09-08 21:14:13.000000000 -0400
@@ -242,6 +242,8 @@
 #define __NR_io_cancel		231
 #endif
 
+#define __NR_setleds		232
+
 #define __NR(n)	#n
 
 
diff -u ppclinux/arch/ppc/kernel/syscalls.c ppclinux_spca5xx_le/arch/ppc/kernel/syscalls.c
--- ppclinux/arch/ppc/kernel/syscalls.c	2005-04-10 22:50:07.000000000 -0400
+++ ppclinux_spca5xx_le/arch/ppc/kernel/syscalls.c	2007-09-09 05:44:35.000000000 -0400
@@ -43,6 +43,8 @@
 #include <asm/ipc.h>
 #include <asm/semaphore.h>
 
+#define DSMG600_SETLEDS_DEBUG
+
 void
 check_bugs(void)
 {
@@ -298,6 +300,92 @@
 	return error;
 }
 
+
+/* not static, because we want to call this from */
+/* sandpoint_setup:sandpoint_power_off()         */
+long priv_sys_setleds(const char * msg, int len)
+{
+	/* HORRID HORRID HORRID!!!                             */
+	/* NEVER use file access in-kernel.  However,          */
+	/* this will only ever get called by sandpoint_setup's */
+	/* sandpoint_power_off() so it's probably ok...that is,*/
+	/* if /dev/ttyS1 is still registered at that time.     */
+	/* Which it isn't, of course.  So, we try /dev/tts/1   */
+
+	/* WARNING: do NOT call printk() while /dev/ttyS1 is   */
+	/*          open. Otherwise, kernel oopses. This is    */
+        /*          reason why it is a REALLY bad idea to do   */
+	/*          file i/o from the kernel                   */
+	struct file *filp;
+	mm_segment_t old_fs;
+	ssize_t rc = 0;
+
+#ifdef DSMG600_SETLEDS_DEBUG
+	printk(KERN_EMERG "core send: %s", msg);
+#endif
+
+	/* do not use /dev/ttyS1, because devfsd may not be running */
+	filp   = filp_open("/dev/tts/1",O_WRONLY,S_IRUSR|S_IWUSR);
+	if (IS_ERR(filp))         return PTR_ERR(filp);
+	if (!(filp->f_op))        return -EFAULT;
+	if (!(filp->f_op->write)) return -EFAULT;
+
+	old_fs = get_fs();
+	set_fs(KERNEL_DS);
+
+	rc = filp->f_op->write(filp,msg,len,&filp->f_pos);
+
+	set_fs(old_fs);
+
+	filp_close(filp, NULL);
+	if (rc < 0)
+		return rc;
+	if (rc < len)
+		return -EFAULT;
+
+#ifdef DSMG600_SETLEDS_DEBUG
+	printk(KERN_EMERG "core sent: %s", msg);
+#endif
+
+	return 0;
+}
+
+asmlinkage int sys_setleds(const char * message, int len)
+{
+	char msg[256];
+	int rc;
+
+	if (!capable(CAP_SYS_ADMIN))
+		return -EPERM;
+	if (len < 0 || len >= sizeof(msg) - 2)
+		return -EINVAL;
+
+	rc = -EFAULT;
+	if (copy_from_user(msg, message, len))
+		return rc;
+
+#ifdef DSMG600_SETLEDS_DEBUG
+	msg[len] = 0;
+	printk("send: %s\n", msg);
+#endif
+
+	msg[len  ] = '\r';
+	msg[len+1] = '\n';
+	msg[len+2] = 0;
+
+	rc = priv_sys_setleds(msg, len+2);
+
+	msg[len] = 0;
+	if (rc)
+		printk("DSMG600 setleds: sending %s failed (%d)\n", msg, rc);
+#ifdef DSMG600_SETLEDS_DEBUG
+	else
+		printk("sent: %s\n", msg);
+#endif
+
+	return rc;
+}
+
 #ifndef CONFIG_PCI
 /*
  * Those are normally defined in arch/ppc/kernel/pci.c. But when CONFIG_PCI is
diff -u ppclinux/arch/ppc/platforms/sandpoint_setup.c ppclinux_spca5xx_le/arch/ppc/platforms/sandpoint_setup.c
--- ppclinux/arch/ppc/platforms/sandpoint_setup.c	2007-09-06 02:30:14.000000000 -0400
+++ ppclinux_spca5xx_le/arch/ppc/platforms/sandpoint_setup.c	2007-09-09 16:36:58.000000000 -0400
@@ -125,6 +125,19 @@
 };
 
 #endif
+
+/* defined in arch/ppc/kernel/syscalls.c */
+long priv_sys_setleds(const char * msg, int len);
+
+static void
+dsmg600_control(const char* s, int len)
+{
+	int rc = priv_sys_setleds(s, len);
+	if (rc)
+		printk(KERN_EMERG "setleds '%s' failed: %d\n", s, rc); 
+}
+
+
 /*
  * Define all of the IRQ senses and polarities.  Taken from the
  * Sandpoint X3 User's manual.
@@ -503,6 +516,15 @@
 static void
 sandpoint_power_off(void)
 {
+	int i;
+
+	printk(KERN_EMERG "about to send ZWC cmd\n");
+	for (i=0;i<500000;i++) ;
+	dsmg600_control("ZWC", 3);
+	for (i=0;i<500000;i++) ;
+	printk(KERN_EMERG "sent ZWC cmd\n");
+	for (i=0;i<500000;i++) ;
+
 	__cli();
 	for(;;);  /* No way to shut power off with software */
 	/* NOTREACHED */
diff -u ppclinux/arch/ppc/platforms/sandpoint_serial.h ppclinux_spca5xx_le/arch/ppc/platforms/sandpoint_serial.h
--- ppclinux/arch/ppc/platforms/sandpoint_serial.h	2005-04-19 23:33:57.000000000 -0400
+++ ppclinux_spca5xx_le/arch/ppc/platforms/sandpoint_serial.h	2007-09-09 16:41:03.000000000 -0400
@@ -40,7 +40,7 @@
 #ifdef CONFIG_SERIAL_MANY_PORTS
 #define RS_TABLE_SIZE  2	//64	jackl
 #else
-#define RS_TABLE_SIZE 1 
+#define RS_TABLE_SIZE 2
 #endif
 
 /* Rate for the 1.8432 Mhz clock for the onboard serial chip */
diff -u ppclinux/arch/ppc/kernel/setup.c ppclinux_spca5xx_le/arch/ppc/kernel/setup.c
--- ppclinux/arch/ppc/kernel/setup.c	2007-09-06 01:49:58.000000000 -0400
+++ ppclinux_spca5xx_le/arch/ppc/kernel/setup.c	2007-09-09 16:38:54.000000000 -0400
@@ -118,10 +118,12 @@
 {
 	ppc_md.restart(cmd);
 }
-  
+
 void machine_power_off(void)
 {
+	printk(KERN_EMERG "ppc: machine_power_off ENTER\n");
 	ppc_md.power_off();
+	printk(KERN_EMERG "ppc: machine_power_off EXIT\n");
 }
   
 void machine_halt(void)
diff -u ppclinux/drivers/char/serial.c ppclinux_spca5xx_le/drivers/char/serial.c
--- ppclinux/drivers/char/serial.c	2005-10-26 09:39:28.000000000 -0400
+++ ppclinux_spca5xx_le/drivers/char/serial.c	2007-09-09 04:27:53.000000000 -0400
@@ -144,7 +144,7 @@
 #undef SERIAL_DEBUG_AUTOCONF
 
 //#define SERIAL_DEBUG_AUTOCONF	1
-//#define SERIAL_DEBUG_OPEN	1
+#define SERIAL_DEBUG_OPEN	1
 //#define SERIAL_DEBUG_INTR 1
 //#define jack_debug	1
 

[Index of Archives]     [Newbies FAQ]     [Linux Kernel Mentors]     [Linux Kernel Development]     [IETF Annouce]     [Git]     [Networking]     [Security]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux RAID]     [Linux SCSI]     [Linux ACPI]
  Powered by Linux