blob: eb8dff98fb5504ca5ea92e4ca421865ac4e3fec4 [file] [log] [blame]
#include <assert.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include "nexus_types.h"
#include "nexus_platform.h"
#include "nexus_gpio.h"
#include "nexus_gpio_init.h"
#include "nexus_pwm.h"
#include "nexus_temp_monitor.h"
#include "nexus_avs.h"
/*
* We're polling at a very high frequency, which is a pain. This would be
* slightly less gross inside the kernel (for less context switching and
* because it could more easily use the tick interrupt instead of polling).
*
* This setting isn't as bad as it sounds, though, because we don't poll
* 100% of the time; we only do it for a fraction of a second every now
* and then.
*/
#define POLL_HZ 2000 // polls per sec
#define USEC_PER_TICK (1000000 / POLL_HZ)
#define PWM_50_KHZ 0x7900
#define PWM_26_KHZ 0x4000
#define PWM_206_HZ 0x0080
#define CHECK(x) do { \
int rv = (x); \
if (rv) { \
fprintf(stderr, "CHECK: %s returned %d\n", #x, rv); \
_exit(99); \
} \
} while (0)
static int platform_limited_leds, platform_b0;
struct Gpio {
NEXUS_GpioType type;
unsigned int pin;
NEXUS_GpioMode mode;
NEXUS_GpioInterrupt interrupt_mode;
NEXUS_GpioHandle handle;
int old_val;
};
struct Pwm {
unsigned int channel;
NEXUS_PwmChannelHandle handle;
int old_percent;
};
struct Gpio led_red = {
NEXUS_GpioType_eAonStandard, 17,
NEXUS_GpioMode_eOutputPushPull, NEXUS_GpioInterrupt_eDisabled, 0, -1
};
struct Gpio led_blue = {
NEXUS_GpioType_eAonStandard, 12,
NEXUS_GpioMode_eOutputPushPull, NEXUS_GpioInterrupt_eDisabled, 0, -1
};
struct Gpio led_activity = {
NEXUS_GpioType_eAonStandard, 13,
NEXUS_GpioMode_eOutputPushPull, NEXUS_GpioInterrupt_eDisabled, 0, -1
};
struct Gpio led_standby = {
NEXUS_GpioType_eAonStandard, 10,
NEXUS_GpioMode_eOutputPushPull, NEXUS_GpioInterrupt_eDisabled, 0, -1
};
struct Gpio reset_button = {
NEXUS_GpioType_eAonStandard, 4,
NEXUS_GpioMode_eInput, NEXUS_GpioInterrupt_eDisabled/*eEdge*/, 0, -1
};
struct Gpio fan_tick = {
NEXUS_GpioType_eStandard, 98,
NEXUS_GpioMode_eInput, NEXUS_GpioInterrupt_eDisabled/*eFallingEdge*/, 0, -1
};
struct Pwm fan_control = { 0, 0, -1 };
// Open the given PWM. You have to do this before writing it.
static void pwm_open(struct Pwm *p) {
NEXUS_PwmChannelSettings settings;
NEXUS_Pwm_GetDefaultChannelSettings(&settings);
settings.eFreqMode = NEXUS_PwmFreqModeType_eConstant;
p->handle = NEXUS_Pwm_OpenChannel(p->channel, &settings);
if (!p->handle) {
fprintf(stderr, "Pwm_Open returned null\n");
_exit(1);
}
}
// Set the given PWM (pulse width modulator) to the given percent duty cycle.
static void set_pwm(struct Pwm *p, int percent) {
if (percent < 0) percent = 0;
if (percent > 100) percent = 100;
if (p->old_percent == percent) return;
p->old_percent = percent;
CHECK(NEXUS_Pwm_SetControlWord(p->handle, PWM_26_KHZ));
CHECK(NEXUS_Pwm_SetPeriodInterval(p->handle, 99));
CHECK(NEXUS_Pwm_SetOnInterval(p->handle, percent));
CHECK(NEXUS_Pwm_Start(p->handle));
}
// Get the CPU temperature. I think it's in Celsius.
static double get_cpu_temperature(void) {
NEXUS_AvsStatus status;
CHECK(NEXUS_GetAvsStatus(&status));
return status.temperature / 100 / 10.0; // round to nearest 0.1
}
// Get the CPU voltage.
static double get_cpu_voltage(void) {
NEXUS_AvsStatus status;
CHECK(NEXUS_GetAvsStatus(&status));
return status.voltage / 10 / 100.0; // round to nearest 0.01
}
// Open the given GPIO pin. You have to do this before reading or writing it.
static void gpio_open(struct Gpio *g) {
NEXUS_GpioSettings settings;
NEXUS_Gpio_GetDefaultSettings(g->type, &settings);
settings.mode = g->mode;
settings.interruptMode = g->interrupt_mode;
settings.value = NEXUS_GpioValue_eLow;
g->handle = NEXUS_Gpio_Open(g->type, g->pin, &settings);
if (!g->handle) {
fprintf(stderr, "Gpio_Open returned null\n");
_exit(1);
}
}
// Write the given GPIO pin.
// I don't actually know what's the difference between eHigh and eMax.
static void set_gpio(struct Gpio *g, int level) {
if (g->old_val == level) {
// If this is the same value as last time, don't do anything, for two
// reasons:
// 1) If you set the gpio too often, it seems to stay low (the led
// stays off).
// 2) If some process other than us is twiddling a led, this way we
// won't interfere with it.
return;
}
g->old_val = level;
NEXUS_GpioValue val;
switch (level) {
case 0: val = NEXUS_GpioValue_eLow; break;
case 1: val = NEXUS_GpioValue_eHigh; break;
case 2: val = NEXUS_GpioValue_eMax; break;
default:
assert(level >= 0);
assert(level <= 2);
val = NEXUS_GpioValue_eLow;
break;
}
NEXUS_GpioSettings settings;
NEXUS_Gpio_GetSettings(g->handle, &settings);
settings.value = val;
NEXUS_Gpio_SetSettings(g->handle, &settings);
}
// Read the given GPIO pin
static NEXUS_GpioValue get_gpio(struct Gpio *g) {
NEXUS_GpioStatus status;
CHECK(NEXUS_Gpio_GetStatus(g->handle, &status));
return status.value;
}
// Turn the leds on or off depending on the bits in fields. Currently
// the bits are:
// 1: red
// 2: blue (green on B0)
// 4: activity (blue)
// 8: standby (bright white)
static void set_leds_from_bitfields(int fields) {
if (platform_limited_leds) {
// GFMS100 only has red and activity lights. Substitute activity for blue
// (they're both blue anyhow) and red+activity (purple) for standby.
if (fields & 0x02) fields |= 0x04;
if (fields & 0x08) fields |= 0x05;
} else if (platform_b0) {
// B0 fat devices are as above (limited_leds).
// B0 fat devices had the leds switched around, and the polarities
// inverted.
fields = ( (fields & 0x8) |
((fields & 0x4) >> 1) |
((fields & 0x2) >> 1) |
((fields & 0x1) << 2));
fields ^= 0x0f;
}
set_gpio(&led_red, (fields & 0x01) ? 1 : 0);
set_gpio(&led_blue, (fields & 0x02) ? 1 : 0);
set_gpio(&led_activity, (fields & 0x04) ? 1 : 0);
set_gpio(&led_standby, (fields & 0x08) ? 1 : 0);
}
// read a file containing a single short string.
// Returns a static buffer. Be careful!
static char *read_file(const char *filename) {
static char buf[1024];
int fd = open(filename, O_RDONLY);
if (fd >= 0) {
size_t got = read(fd, buf, sizeof(buf) - 1);
buf[got] = '\0';
close(fd);
return buf;
}
buf[0] = '\0';
return buf;
}
// create the given (empty) file.
static void create_file(const char *filename) {
// use O_EXCL here to save a close() syscall when it already exists
int fd = open(filename, O_WRONLY|O_CREAT|O_EXCL, 0666);
if (fd >= 0) close(fd);
}
// write a file containing the given string.
static void write_file(const char *filename, const char *content) {
char *tmpname = malloc(strlen(filename) + 4 + 1);
sprintf(tmpname, "%s.tmp", filename);
int fd = open(tmpname, O_WRONLY|O_CREAT|O_TRUNC, 0666);
if (fd >= 0) {
write(fd, content, strlen(content));
close(fd);
rename(tmpname, filename);
}
free(tmpname);
}
// write a file containing just a single integer value (as a string, not
// binary)
static void write_file_int(const char *filename,
long long *oldv, long long newv) {
if (!oldv || *oldv != newv) {
char buf[128];
snprintf(buf, sizeof(buf), "%lld", newv);
buf[sizeof(buf)-1] = 0;
write_file(filename, buf);
if (oldv) *oldv = newv;
}
}
// write a file containing just a single floating point value (as a string,
// not binary)
static void write_file_float(const char *filename,
double *oldv, double newv) {
if (!oldv || *oldv != newv) {
char buf[128];
snprintf(buf, sizeof(buf), "%.2f", newv);
buf[sizeof(buf)-1] = 0;
write_file(filename, buf);
if (oldv) *oldv = newv;
}
}
// read led_sequence from the given file. For example, if a file contains
// 0 1 0 2 0 0x0f
// that means 1/6 of a second off, then red, then off, then blue, then off,
// then all the lights on at once.
static char led_sequence[16];
static unsigned led_sequence_len = 1;
static void read_led_sequence_file(const char *filename) {
char *buf = read_file(filename), *p;
led_sequence_len = 0;
while ((p = strsep(&buf, " \t\n\r")) != NULL &&
led_sequence_len <= sizeof(led_sequence)/sizeof(led_sequence[0])) {
if (!*p) continue;
led_sequence[led_sequence_len++] = strtol(p, NULL, 0);
}
if (!led_sequence_len) {
led_sequence[0] = 1; // red = error
led_sequence_len = 1;
}
}
// switch to the next led combination in led_sequence.
static void led_sequence_update(long long frac) {
int i = led_sequence_len * frac / 1000;
if (i >= (int)led_sequence_len)
i = led_sequence_len;
if (i < 0)
i = 0;
// if the 'activity' file exists, unlink() will succeed, giving us exactly
// one inversion of the activity light. That causes exactly one delightful
// blink.
int activity_toggle = (unlink("activity") == 0) ? 0x04 : 0;
set_leds_from_bitfields(led_sequence[i] ^ activity_toggle);
}
// Same as time(), but in milliseconds instead.
static long long msec_now(void) {
struct timespec ts;
CHECK(clock_gettime(CLOCK_MONOTONIC, &ts));
return ((long long)ts.tv_sec) * 1000 + (ts.tv_nsec / 1000000);
}
// The offset of msec_now() vs. wall clock time.
// Don't use this for anything important, since you can't trust wall clock
// time on our devices. But it's useful for syncing LED blinking between
// devices. Because it's prettier.
static long long msec_offset(void) {
long long mono = msec_now();
struct timeval tv;
gettimeofday(&tv, NULL);
return ((mono - (tv.tv_usec / 1000)) + 1000) % 1000;
}
static volatile int shutdown_sig = 0;
static void sig_handler(int sig) {
shutdown_sig = sig;
signal(sig, SIG_DFL);
// even in case of a segfault, we still want to try to shut down
// politely so we can fix the fan speed etc. writev() is a syscall
// so this sequence should be safe since it has no outside dependencies.
// fprintf() is not 100% safe in a signal handler.
char buf[] = {
'0' + (sig / 100 % 10),
'0' + (sig / 10 % 10),
'0' + (sig % 10),
};
struct iovec iov[] = {
{ "exiting on signal ", 18 },
{ buf, sizeof(buf)/sizeof(buf[0]) },
{ "\n", 1 },
};
writev(2, iov, sizeof(iov)/sizeof(iov[0]));
}
void run_gpio_mailbox(void) {
platform_limited_leds = (0 == strncmp(read_file("/etc/platform"),
"GFMS100", 7));
platform_b0 = (NULL != strstr(read_file("/proc/cpuinfo"), "BCM7425B0"));
gpio_open(&led_standby);
gpio_open(&led_red);
gpio_open(&led_activity);
gpio_open(&led_blue);
gpio_open(&reset_button);
gpio_open(&fan_tick);
pwm_open(&fan_control);
// close any extra fds, especially /dev/brcm0. That way we're
// certain we won't interfere with any other nexus process's interrupt
// handling. Only one process can be doing interrupt handling at a time.
for (int i = 3; i < 100; i++)
close(i);
fprintf(stderr, "gpio mailbox running.\n");
write_file_int("/var/run/gpio-mailbox", NULL, getpid());
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
signal(SIGSEGV, sig_handler);
signal(SIGFPE, sig_handler);
int inner_loop_ticks = 0, msec_per_led = 0;
int reads = 0, fan_flips = 0, fan_loop_count = 0;
long long last_time = 0, last_print_time = msec_now(),
last_led = 0, reset_start = 0, fan_loop_time = 0, offset = msec_offset();
long long fanspeed = -42, reset_amt = -42, readyval = -42;
double cpu_temp = -42.0, cpu_volts = -42.0;
int wantspeed_warned = -42, wantspeed = 0;
while (!shutdown_sig) {
long long now = msec_now();
// blink the leds
if (now - last_led >= msec_per_led) {
read_led_sequence_file("leds");
assert(led_sequence_len > 0);
inner_loop_ticks = POLL_HZ / led_sequence_len + 1;
while (inner_loop_ticks > POLL_HZ / 16) {
// make sure we poll at least every 1/8 of a second, or else the
// activity light won't blink impressively enough.
inner_loop_ticks /= 2;
}
msec_per_led = 1000 / led_sequence_len + 1;
last_led = now;
offset = msec_offset();
create_file("leds-ready");
}
led_sequence_update((now + 1000 - offset) % 1000);
if (now - last_time > 2000) {
// set the fan speed control
char *wantspeed_str = read_file("fanpercent");
if (wantspeed_str[0]) {
wantspeed = strtol(wantspeed_str, NULL, 0);
if (wantspeed < 0 || wantspeed > 100) {
if (wantspeed_warned != wantspeed) {
fprintf(stderr, "gpio/fanpercent (%d) is invalid: must be 0-100\n",
wantspeed);
wantspeed_warned = wantspeed;
}
wantspeed = 100;
} else if (wantspeed < 100 && cpu_temp >= 100.0) {
if (wantspeed_warned != wantspeed) {
fprintf(stderr,
"DANGER: fanpercent (%d) is too low for CPU temp %.2f; "
"using 100%%.\n", wantspeed, cpu_temp);
wantspeed_warned = wantspeed;
}
wantspeed = 100;
} else {
wantspeed_warned = -42;
}
} else {
if (wantspeed_warned != 1)
fprintf(stderr, "gpio/fanpercent is empty: using default value\n");
wantspeed_warned = 1;
wantspeed = 100;
}
set_pwm(&fan_control, wantspeed);
// capture the fan cycle counter
write_file_int("fanspeed", &fanspeed,
fan_flips * 1000 / (fan_loop_time + 1));
fan_flips = fan_loop_time = 0;
// capture the CPU temperature and voltage
write_file_float("cpu_temperature", &cpu_temp, get_cpu_temperature());
write_file_float("cpu_voltage", &cpu_volts, get_cpu_voltage());
last_time = now;
}
if (now - last_print_time >= 6000) {
fprintf(stderr,
"fan:%lld/sec:%d%% reads:%d button:%d temp:%.2f volts:%.2f\n",
fanspeed, wantspeed, reads,
get_gpio(&reset_button), cpu_temp, cpu_volts);
reads = 0;
last_print_time = now;
}
// handle the reset button
int reset = !get_gpio(&reset_button); // 0x1 means *not* pressed
if (reset) {
if (!reset_start) reset_start = now - 1;
write_file_int("reset_button_msecs", &reset_amt, now - reset_start);
} else {
if (reset_amt) unlink("reset_button_msecs");
reset_amt = reset_start = 0;
}
// this is last. it indicates we've made it once through the loop,
// so all the files in /tmp/gpio have been written at least once.
write_file_int("ready", &readyval, 1);
// poll for fan ticks. This is a bit complicated since we want to be
// sure to count the exact time for an integer number of ticks.
fan_loop_count = (fan_loop_count + 1) % 16;
if (!fan_loop_count) {
long long start = 0, end = 0;
int start_fan = get_gpio(&fan_tick), last_fan = start_fan;
for (int tick = 0; tick < inner_loop_ticks; tick++) {
int cur_fan = get_gpio(&fan_tick);
if (last_fan != cur_fan && start_fan == cur_fan) {
if (!start) {
start = msec_now();
} else {
fan_flips++;
end = msec_now();
}
}
reads++;
last_fan = cur_fan;
if (shutdown_sig) break;
usleep(USEC_PER_TICK);
}
fan_loop_time += end - start;
} else {
// no need to poll *every* time.
// For the last tick of each second, adjust it slightly so our LED
// blinks can be aligned on the one-second boundary.
long long time_to_boundary = 1000000 - ((now - offset) * 1000) % 1000000;
long long delay = USEC_PER_TICK * inner_loop_ticks;
if (delay > time_to_boundary) delay = time_to_boundary;
usleep(delay);
}
}
// shut down cleanly
set_leds_from_bitfields(1); // red light to indicate a problem
set_pwm(&fan_control, 100); // for safety
// do *not* clean up nicely in the child; we use _exit() instead of
// returning or calling exit(). A polite shutdown is what the parent
// process should have done. No need to do it twice.
if (shutdown_sig > 0) {
kill(getpid(), shutdown_sig);
}
_exit(0);
}
int main(void) {
int status = 98;
int pipefds[2];
fprintf(stderr, "starting gpio mailbox in /tmp/gpio.\n");
mkdir("/tmp/gpio", 0775);
if (chdir("/tmp/gpio") != 0) {
perror("chdir /tmp/gpio");
return 1;
}
mkdir("/tmp/leds", 0775);
NEXUS_PlatformSettings platform_settings;
NEXUS_Platform_GetDefaultSettings(&platform_settings);
platform_settings.openFrontend = false;
if (NEXUS_Platform_Init(&platform_settings) != 0)
goto end;
if (pipe(pipefds) != 0) {
perror("pipe");
_exit(99);
}
// Fork into the background so we can shut down most of our copy of nexus.
// Otherwise it leaves things like the video threads running, which results
// in a mess. But it happens that the gpio/pwm stuff is just done through
// mmap, which will be inherited across a fork, unlike all the extra junk
// threads.
pid_t pid = fork();
if (pid < 0) {
perror("fork");
_exit(99);
} else if (pid == 0) {
// child process. First, wait for the parent to finish its setup.
char buf[1];
close(pipefds[1]);
if (read(pipefds[0], buf, 1) != 0) {
// this should just always return 0 == EOF.
perror("pipe-read");
_exit(99);
}
close(pipefds[0]);
run_gpio_mailbox();
_exit(0);
}
// parent process. Uninit nexus here, to kill the unnecessary threads.
NEXUS_Platform_Uninit();
// release the child process now that our copy of Nexus is shut down
close(pipefds[0]);
close(pipefds[1]);
// now wait for the child process to exit so we can propagate its exit
// code to our own parent, who can make decisions about restarting.
while (waitpid(pid, &status, 0) != pid) { }
end:
// normally the child process does this step.
//
// do it again here just in case the child process dies early; the boot
// process will wait on this file, and we don't want it to get jammed
// forever.
write_file_int("/var/run/gpio-mailbox", NULL, getpid());
exit(status);
}