First commit: Publish project as open source.

This commit is contained in:
Keyitdev
2026-07-28 12:52:19 +02:00
commit cc51c61859
27 changed files with 2995 additions and 0 deletions
+396
View File
@@ -0,0 +1,396 @@
#define _GNU_SOURCE
#include "commands.h"
#include "common.h"
#include "ec.h"
#include "fan.h"
#include "healthy.h"
#include "mmio.h"
#include "util.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static void print_model(void)
{
char vendor[128], product[128];
if (read_line_file("/sys/class/dmi/id/sys_vendor", vendor, sizeof vendor) &&
read_line_file("/sys/class/dmi/id/product_name", product, sizeof product))
printf("Laptop model: %s %s (DMI)\n", vendor, product);
else if (read_line_file("/sys/class/dmi/id/product_name", product,
sizeof product))
printf("Laptop model: %s (DMI)\n", product);
else
printf("Laptop model: unreadable (/sys/class/dmi/id)\n");
}
static void print_identity(hy_t *h, mmio_t *m)
{
uint8_t version;
int mj, mn;
print_model();
mj = mmio_at(m, ECMJ);
mn = mmio_at(m, ECMN);
printf("Embedded controller version: %d.%d (0x%08lX = %02X %02X)\n", mj, mn,
ECMJ, (unsigned)mj, (unsigned)mn);
if (hy_version(h, &version))
printf("Healthy table version: unreadable (0xBB 50)\n");
else
printf("Healthy table version: %d (0xBB 50)\n", version);
}
static void print_project(void)
{
printf("\n");
printf("%s version: %s\n", APP_NAME, APP_VERSION);
printf("Source code: %s\n", APP_URL);
printf("Support the project: %s\n", APP_KOFI);
printf("License: %s\n", APP_LICENSE);
printf("%s\n", APP_COPYRIGHT);
}
int cmd_version(args_t *a)
{
ec_t ec;
hy_t h;
mmio_t mm;
if (geteuid() == 0) {
ec_open(&ec, a->cmd_port, a->data_port, a->verbose);
h.ec = &ec;
h.gap = a->gap;
mmio_open(&mm, ERM2);
print_identity(&h, &mm);
mmio_close(&mm);
ec_close(&ec);
} else {
print_model();
printf("Embedded controller version: root required\n");
printf("Healthy table version: root required\n");
}
print_project();
return 0;
}
int cmd_fan_info(args_t *a)
{
ec_t ec;
hy_t h;
mmio_t mm;
int duty[MAX_FANS], mode[MAX_FANS], rpm[MAX_FANS];
int hi[MAX_FANS], lo[MAX_FANS], ref[MAX_FANS];
int count, idx, agree = 0, checked = 0, spinning = 0, rc = 0;
bool uniform = true;
ec_open(&ec, a->cmd_port, a->data_port, a->verbose);
h.ec = &ec;
h.gap = a->gap;
mmio_open(&mm, ERM2);
count = fan_count_or_default(&h);
if (count < 1 || count > MAX_FANS) {
printf("Fan count: unreadable (0x30 out of range)\n");
rc = 1;
goto out;
}
printf("Fan count: %d (0x30=%d)\n", count, count);
for (idx = 0; idx < count; idx++) {
uint8_t d = 0, m = 0, rh = 0, rl = 0;
duty[idx] = -1;
mode[idx] = -1;
rpm[idx] = -1;
ref[idx] = -1;
hi[idx] = 0;
lo[idx] = 0;
if (hy_select(&h, idx))
continue;
if (!hy_read(&h, REG_PWM_DUTY, &d))
duty[idx] = d;
if (!hy_read(&h, REG_TEST_MODE, &m))
mode[idx] = m;
if (!hy_read(&h, REG_RPM_HI, &rh) && !hy_read(&h, REG_RPM_LO, &rl)) {
hi[idx] = rh;
lo[idx] = rl;
rpm[idx] = (rh << 8) | rl;
}
if (idx < 2)
ref[idx] = mmio_be16(&mm, TACH_OFFSET + 2 * (unsigned long)idx);
if (ref[idx] > 0)
spinning++;
}
for (idx = 1; idx < count; idx++)
if (mode[idx] != mode[0])
uniform = false;
if (!uniform)
printf("Fan control: differs between fans (see below)\n");
else if (mode[0] < 0)
printf("Fan control: unreadable (0x31)\n");
else
printf("Fan control: %s (0x31=%d)\n", mode_name(mode[0]), mode[0]);
for (idx = 0; idx < count; idx++) {
unsigned long addr = ERM2 + TACH_OFFSET + 2 * (unsigned long)idx;
int expect = ref[idx];
printf("\nfan%d:\n", idx);
if (duty[idx] < 0)
printf(" PWM duty: unreadable (0x35)\n");
else
printf(" PWM duty: %d (%d%%) (0x35=%d)\n", duty[idx],
duty_to_percent(duty[idx]), duty[idx]);
if (!uniform)
printf(" Fan control: %s (0x31=%d)\n", mode_name(mode[idx]),
mode[idx]);
if (rpm[idx] < 0)
printf(" Fan speed reg: unreadable (0x34/0x33)\n");
else
printf(" Fan speed reg: %d rpm (0x34/0x33 = %02X %02X)\n", rpm[idx],
(unsigned)hi[idx], (unsigned)lo[idx]);
if (expect < 0)
printf(" Fan speed mmio: no aperture slot\n");
else
printf(" Fan speed mmio: %d rpm (0x%08lX)\n", expect, addr);
if (rpm[idx] < 0 || expect < 0) {
printf(" Verdict: UNKNOWN\n");
continue;
}
checked++;
if (abs(rpm[idx] - expect) <= TACH_TOL) {
agree++;
printf(" Verdict: MATCH\n");
} else {
printf(" Verdict: MISMATCH\n");
}
}
printf("\n");
if (!spinning) {
printf("Result: both tachometers read zero, spin the fans up and retry.\n");
rc = 1;
} else if (checked == count && agree == count) {
printf("Result: VALIDATED against the MMIO aperture.\n");
printf("If the program works on your device and it is not listed\nin the tested devices section on github, please open an issue.\n");
} else {
printf("Result: NOT VALIDATED, do not write anything.\n");
rc = 1;
}
out:
mmio_close(&mm);
ec_close(&ec);
return rc;
}
int cmd_temps_info(args_t *a)
{
mmio_t mm;
int cpu, board;
(void)a;
mmio_open(&mm, ERAM);
cpu = mmio_at(&mm, CTMP);
board = mmio_at(&mm, CLOT);
mmio_close(&mm);
printf("CPU: %d C (0x%08lX = %02X, ERAM+0x58 CTMP)\n", cpu, CTMP,
(unsigned)cpu);
printf("Board: %d C (0x%08lX = %02X, ERAM+0x01 CLOT)\n", board, CLOT,
(unsigned)board);
printf("Max: %d C (higher of the two)\n", imax(cpu, board));
return 0;
}
int cmd_fan_speed(args_t *a)
{
ec_t ec;
hy_t h;
uint8_t mode;
int n, idx;
ec_open(&ec, a->cmd_port, a->data_port, a->verbose);
h.ec = &ec;
h.gap = a->gap;
n = fan_count_or_default(&h);
if (n < 1 || n > MAX_FANS)
n = 2;
if (hy_read(&h, REG_TEST_MODE, &mode))
printf("Fan control: unreadable (0x31)\n");
else
printf("Fan control: %s (0x31=%d)\n", mode_name(mode), mode);
for (idx = 0; idx < n; idx++) {
int rpm;
if (hy_select(&h, idx) || hy_rpm(&h, &rpm))
printf("fan %d: speed unreadable\n", idx);
else
printf("fan %d: %d rpm\n", idx, rpm);
}
ec_close(&ec);
return 0;
}
static int verify(hy_t *h, int duty, const int *pending, int npending,
int *failed)
{
int i, nfailed = 0;
for (i = 0; i < npending; i++) {
int idx = pending[i], rb, md;
bool ok;
printf("fan %d:\n", idx);
if (readback(h, idx, &rb, &md)) {
printf(" Set: FAILED, readback error: %s\n", ec_error);
printf(" PWM duty: unreadable (0x35)\n");
ok = false;
} else {
ok = duty_close(rb, duty);
if (ok)
printf(" Set: OK\n");
else
printf(" Set: FAILED, wanted duty %d\n", duty);
printf(" PWM duty: %d (%d%%) (0x35=%d)\n", rb, duty_to_percent(rb),
rb);
}
if (!ok)
failed[nfailed++] = idx;
}
return nfailed;
}
static int drive(hy_t *h, int duty, const int *targets, int ntargets,
args_t *a, int *left)
{
int pending[MAX_FANS], npending = ntargets;
int failed[MAX_FANS], nfailed;
int attempt, i;
char list[64];
memcpy(pending, targets, sizeof(int) * (size_t)ntargets);
for (attempt = 1; attempt <= a->retries; attempt++) {
join_ints(list, sizeof list, pending, npending);
printf("Attempt %d/%d, fans %s, duty %d.\n", attempt, a->retries, list, duty);
for (i = 0; i < npending; i++)
if (apply_duty(h, pending[i], duty, a->order))
printf(" fan %d FAIL, write failed: %s.\n", pending[i], ec_error);
if (a->no_verify)
return 0;
nfailed = verify(h, duty, pending, npending, failed);
if (!nfailed)
return 0;
memcpy(pending, failed, sizeof(int) * (size_t)nfailed);
npending = nfailed;
}
memcpy(left, pending, sizeof(int) * (size_t)npending);
return npending;
}
static int run_set(args_t *a, int duty)
{
ec_t ec;
hy_t h;
mmio_t mm;
int count, targets[MAX_FANS], ntargets, left[MAX_FANS], nleft, fans[2], i;
char list[64];
if (duty < 0 || duty > 255)
die("Duty must be 0..255, or -1 to hand control back to the EC.");
ec_open(&ec, a->cmd_port, a->data_port, a->verbose);
h.ec = &ec;
h.gap = a->gap;
mmio_open(&mm, ERM2);
count = fan_count_or_default(&h);
if (count < 1 || count > MAX_FANS) {
mmio_close(&mm);
ec_close(&ec);
die("Fan count from 0x30 is implausible, refusing to write.");
}
mmio_fans(&mm, fans);
printf("Fan count: %d, speeds before: %d/%d rpm.\n", count, fans[0], fans[1]);
if (a->index < 0) {
ntargets = count;
for (i = 0; i < count; i++)
targets[i] = i;
} else {
ntargets = 1;
targets[0] = a->index;
}
nleft = drive(&h, duty, targets, ntargets, a, left);
mmio_fans(&mm, fans);
printf("Fan count: %d, speeds after: %d/%d rpm.\n", count, fans[0], fans[1]);
mmio_close(&mm);
ec_close(&ec);
if (nleft) {
join_ints(list, sizeof list, left, nleft);
printf("\n");
printf("Fans %s did not take the setting after %d attempts.\n", list,
a->retries);
return 1;
}
return 0;
}
static int run_release(args_t *a)
{
ec_t ec;
hy_t h;
int left[MAX_FANS];
int n, nleft;
char list[64];
ec_open(&ec, a->cmd_port, a->data_port, a->verbose);
h.ec = &ec;
h.gap = a->gap;
n = fan_count_or_default(&h);
if (n < 1 || n > MAX_FANS)
n = 2;
hand_back(&h, n);
nleft = still_manual(&h, n, left);
if (nleft) {
join_ints(list, sizeof list, left, nleft);
printf("Fans %s still in manual, retrying.\n", list);
hand_back(&h, n);
nleft = still_manual(&h, n, left);
}
ec_close(&ec);
if (nleft) {
join_ints(list, sizeof list, left, nleft);
printf("Fans %s are still in manual, the EC did not take the release.\n",
list);
return 1;
}
printf("Handed fan control back to the EC.\n");
return 0;
}
int cmd_set(args_t *a)
{
if (a->duty == RELEASE_ARG)
return run_release(a);
return run_set(a, a->duty);
}
int cmd_setp(args_t *a)
{
int duty;
if (a->percent == RELEASE_ARG)
return run_release(a);
if (a->percent < 0 || a->percent > 100)
die("Percent must be 0..100, or -1 to hand control back to the EC.");
duty = percent_to_duty(a->percent);
printf("%d%% maps to duty %d.\n", a->percent, duty);
return run_set(a, duty);
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef AFC_COMMANDS_H
#define AFC_COMMANDS_H
#include "common.h"
int cmd_version(args_t *a);
int cmd_fan_info(args_t *a);
int cmd_fan_speed(args_t *a);
int cmd_temps_info(args_t *a);
int cmd_set(args_t *a);
int cmd_setp(args_t *a);
#endif
+77
View File
@@ -0,0 +1,77 @@
#ifndef AFC_COMMON_H
#define AFC_COMMON_H
#include <stdbool.h>
#define DATA_PORT 0x25C
#define CMD_PORT 0x25D
#define PREAMBLE 0xFF
#define CMD_VERSION 0xBB
#define CMD_TABLE 0xDD
#define TBL_READ 0x02
#define TBL_WRITE 0x82
#define REG_FAN_COUNT 0x30
#define REG_TEST_MODE 0x31
#define REG_FAN_INDEX 0x32
#define REG_RPM_LO 0x33
#define REG_RPM_HI 0x34
#define REG_PWM_DUTY 0x35
#define ERM2 0xFEDD8B00UL
#define ERAM 0xFEDD8300UL
#define PAGE_MASK (~0xFFFUL)
#define CTMP (ERAM + 0x58)
#define CLOT (ERAM + 0x01)
#define ECMJ (ERAM + 0xE4)
#define ECMN (ERAM + 0xE5)
#define TACH_OFFSET 0x7C
#define APP_NAME "asus-fan-control-ec"
#define APP_VERSION "v1.0.0"
#define APP_URL "https://github.com/Keyitdev/asus-fan-control-ec"
#define APP_KOFI "https://ko-fi.com/keyitdev"
#define APP_LICENSE "GPLv3+"
#define APP_COPYRIGHT "Copyright (C) 2026 Keyitdev."
#define DEFAULT_CONFIG "/etc/asus-fan-curve.conf"
#define PANIC_TEMP 96
#define HYSTERESIS 3
#define INTERVAL 3.0
#define OBF 0x01
#define IBF 0x02
#define POLL_LIMIT 1000
#define POLL_DELAY 0.0001
#define RETRIES 2
#define IO_GAP 0.02
#define TOL_PCT 5
#define TOL_MIN 3
#define READ_RETRIES 3
#define TACH_TOL 64
#define RELEASE_ARG (-1)
#define CURVE_UNSET (-2)
#define MAX_FANS 2
#define MAX_POINTS 64
enum { ORDER_MODE_FIRST, ORDER_DUTY_FIRST };
enum { SENSOR_CPU, SENSOR_BOARD, SENSOR_MAX };
enum { APPLY_FAILED, APPLY_CLEAN, APPLY_RETRIED };
typedef struct {
unsigned cmd_port;
unsigned data_port;
double gap;
bool verbose;
int index;
int retries;
bool no_verify;
int order;
int duty;
int percent;
const char *config;
double interval;
int hysteresis;
int panic_temp;
int sensor;
bool once;
bool dry_run;
bool silent;
} args_t;
#endif
+296
View File
@@ -0,0 +1,296 @@
#define _GNU_SOURCE
#include "curve.h"
#include "ec.h"
#include "fan.h"
#include "healthy.h"
#include "mmio.h"
#include "util.h"
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
static volatile sig_atomic_t stop_flag = 0;
static int point_cmp(const void *a, const void *b)
{
const point_t *p = a, *q = b;
if (p->temp != q->temp)
return p->temp < q->temp ? -1 : 1;
if (p->percent != q->percent)
return p->percent < q->percent ? -1 : 1;
return 0;
}
static int load_curve(const char *path, point_t *points)
{
FILE *f = fopen(path, "r");
char line[256];
int n = 0, num = 0, i;
if (!f)
die("Config not found: %s.", path);
while (fgets(line, sizeof line, f)) {
char *hash, *sep, *end, *rest;
long percent, temp;
num++;
hash = strchr(line, '#');
if (hash)
*hash = '\0';
rest = line;
while (*rest && isspace((unsigned char)*rest))
rest++;
end = rest + strlen(rest);
while (end > rest && isspace((unsigned char)end[-1]))
*--end = '\0';
if (!*rest)
continue;
sep = strpbrk(rest, ",;");
if (!sep || strpbrk(sep + 1, ",;")) {
fclose(f);
die("%s:%d: expected 'percent,temp'.", path, num);
}
*sep = '\0';
errno = 0;
percent = strtol(rest, &end, 10);
while (end && *end && isspace((unsigned char)*end))
end++;
if (errno || end == rest || *end) {
fclose(f);
die("%s:%d: not a pair of integers.", path, num);
}
rest = sep + 1;
while (*rest && isspace((unsigned char)*rest))
rest++;
errno = 0;
temp = strtol(rest, &end, 10);
while (end && *end && isspace((unsigned char)*end))
end++;
if (errno || end == rest || *end) {
fclose(f);
die("%s:%d not a pair of integers", path, num);
}
if ((percent < 0 && percent != RELEASE_ARG) || percent > 100) {
fclose(f);
die("%s:%d: percent must be 0..100, or -1 for EC control.", path, num);
}
if (temp < 0 || temp > 120) {
fclose(f);
die("%s:%d: temperature out of range.", path, num);
}
if (n == MAX_POINTS) {
fclose(f);
die("%s has too many points.", path);
}
points[n].temp = (int)temp;
points[n].percent = (int)percent;
n++;
}
fclose(f);
if (!n)
die("%s has no usable lines.", path);
qsort(points, (size_t)n, sizeof points[0], point_cmp);
for (i = 1; i < n; i++)
if (points[i].temp == points[i - 1].temp)
die("%s has duplicate temperatures.", path);
return n;
}
static const char *percent_text(char *buf, size_t size, int percent)
{
if (percent == RELEASE_ARG)
snprintf(buf, size, "EC (auto)");
else
snprintf(buf, size, "%d%%", percent);
return buf;
}
static void describe_curve(const point_t *points, int n)
{
char text[16];
int i;
if (points[0].temp > 0)
printf(" below %d C -> 0%%\n", points[0].temp);
for (i = 0; i < n; i++) {
percent_text(text, sizeof text, points[i].percent);
if (i + 1 < n)
printf(" %d-%d C -> %s\n", points[i].temp, points[i + 1].temp, text);
else
printf(" %d C and above -> %s\n", points[i].temp, text);
}
}
static int curve_band(const point_t *points, int n, int temp, int current,
int hysteresis)
{
int band = -1, keep = -1, i;
for (i = 0; i < n; i++) {
if (temp >= points[i].temp)
band = i;
if (temp + hysteresis >= points[i].temp)
keep = i;
}
if (current == CURVE_UNSET || band >= current)
return band;
return imax(band, imin(current, keep));
}
static int band_percent(const point_t *points, int band)
{
return band < 0 ? 0 : points[band].percent;
}
static int apply_percent(hy_t *h, int count, int percent, args_t *a)
{
int duty = percent_to_duty(percent);
int marks[MAX_FANS];
int attempt, r, idx;
bool retried = false;
for (attempt = 1; attempt <= a->retries; attempt++) {
bool all_seen = false, agreed = true;
for (idx = 0; idx < count; idx++)
apply_duty(h, idx, duty, a->order);
for (r = 1; r <= READ_RETRIES; r++) {
all_seen = true;
for (idx = 0; idx < count; idx++) {
int rb, md;
marks[idx] = readback(h, idx, &rb, &md) ? -1 : rb;
if (marks[idx] < 0)
all_seen = false;
}
if (all_seen)
break;
retried = true;
if (r < READ_RETRIES)
printf(" Readback failed, retrying read %d/%d.\n", r + 1,
READ_RETRIES);
}
if (all_seen) {
for (idx = 1; idx < count; idx++)
if (marks[idx] != marks[0])
agreed = false;
if (agreed && duty_close(marks[0], duty))
return retried ? APPLY_RETRIED : APPLY_CLEAN;
}
if (attempt < a->retries) {
retried = true;
printf(" Setting not confirmed, retrying write %d/%d.\n",
attempt + 1, a->retries);
}
}
return APPLY_FAILED;
}
static void on_stop(int sig)
{
(void)sig;
stop_flag = 1;
}
int cmd_curve(args_t *a)
{
point_t points[MAX_POINTS];
ec_t ec;
hy_t h;
mmio_t mm;
int n, count, current = CURVE_UNSET, band = CURVE_UNSET;
struct sigaction sa;
n = load_curve(a->config, points);
printf("Curve loaded from %s.\n", a->config);
describe_curve(points, n);
printf("sensor=%s interval=%.1fs hysteresis=%dC panic=%dC\n",
sensor_name(a->sensor),
a->interval, a->hysteresis, a->panic_temp);
if (a->dry_run)
return 0;
ec_open(&ec, a->cmd_port, a->data_port, a->verbose);
h.ec = &ec;
h.gap = a->gap;
mmio_open(&mm, ERM2);
count = fan_count_or_default(&h);
if (count < 1 || count > MAX_FANS) {
mmio_close(&mm);
ec_close(&ec);
die("Fan count from 0x30 is implausible, refusing to run.");
}
memset(&sa, 0, sizeof sa);
sa.sa_handler = on_stop;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGHUP, &sa, NULL);
for (;;) {
int cpu, board, temp, want, next, fans[2], ticks, i;
bool panic;
const char *status, *mode;
char clock[16], target[8];
time_t now;
mmio_temps(&mm, &cpu, &board);
temp = sensor_pick(a->sensor, cpu, board);
next = curve_band(points, n, temp, band, a->hysteresis);
want = band_percent(points, next);
panic = temp >= a->panic_temp;
if (panic)
want = 100;
if (want != current) {
int applied = want == RELEASE_ARG ? release_to_ec(&h, count)
: apply_percent(&h, count, want, a);
if (applied != APPLY_FAILED) {
current = want;
band = next;
}
status = applied == APPLY_FAILED ? "FAILED"
: applied == APPLY_RETRIED ? "retried" : "applied";
} else {
band = next;
status = "hold";
}
mode = current == RELEASE_ARG ? "EC (auto)"
: current == CURVE_UNSET ? "unknown" : "manual";
if (want == RELEASE_ARG)
snprintf(target, sizeof target, "%4s", "--");
else
snprintf(target, sizeof target, "%3d%%", want);
mmio_fans(&mm, fans);
now = time(NULL);
strftime(clock, sizeof clock, "%H:%M:%S", localtime(&now));
if (!a->silent) {
printf("%s temp=%dC target=%s %-9s %-7s fan0=%5d fan1=%5d%s\n", clock,
temp, target, mode, status, fans[0], fans[1],
panic ? " PANIC" : "");
fflush(stdout);
}
if (a->once || stop_flag)
break;
ticks = (int)(a->interval * 10);
for (i = 0; i < ticks && !stop_flag; i++)
nsleep(0.1);
if (stop_flag)
break;
}
if (!a->once) {
hand_back(&h, count);
printf("Handed fan control back to the EC.\n");
}
mmio_close(&mm);
ec_close(&ec);
return 0;
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef AFC_CURVE_H
#define AFC_CURVE_H
#include "common.h"
typedef struct {
int temp;
int percent;
} point_t;
int cmd_curve(args_t *a);
#endif
+163
View File
@@ -0,0 +1,163 @@
#define _GNU_SOURCE
#include "ec.h"
#include "common.h"
#include "util.h"
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#if defined(__i386__) || defined(__x86_64__)
#include <sys/io.h>
#define HAVE_PORT_IO 1
#else
#define HAVE_PORT_IO 0
#endif
char ec_error[64] = "";
int ec_open(ec_t *e, unsigned cmd_port, unsigned data_port, bool verbose)
{
e->cmd_port = cmd_port;
e->data_port = data_port;
e->verbose = verbose;
e->fd = -1;
e->direct = false;
#if HAVE_PORT_IO
if (cmd_port < 0x400 && data_port < 0x400 &&
ioperm(cmd_port, 1, 1) == 0 && ioperm(data_port, 1, 1) == 0) {
e->direct = true;
return 0;
}
#endif
e->fd = open("/dev/port", O_RDWR);
if (e->fd < 0)
die("Cannot open /dev/port: %s.", strerror(errno));
return 0;
}
void ec_close(ec_t *e)
{
if (e->fd >= 0)
close(e->fd);
e->fd = -1;
}
static uint8_t ec_in8(ec_t *e, unsigned port)
{
uint8_t v;
#if HAVE_PORT_IO
if (e->direct)
return inb((unsigned short)port);
#endif
if (pread(e->fd, &v, 1, (off_t)port) != 1)
die("Cannot read port 0x%03X: %s.", port, strerror(errno));
return v;
}
static void ec_out8(ec_t *e, unsigned port, uint8_t val)
{
if (e->verbose)
printf(" out 0x%03X <- %02X\n", port, (unsigned)val);
#if HAVE_PORT_IO
if (e->direct) {
outb(val, (unsigned short)port);
return;
}
#endif
if (pwrite(e->fd, &val, 1, (off_t)port) != 1)
die("Cannot write port 0x%03X: %s.", port, strerror(errno));
}
static uint8_t ec_status(ec_t *e)
{
return ec_in8(e, e->cmd_port);
}
static int ec_timeout(const char *what)
{
snprintf(ec_error, sizeof ec_error, "%s", what);
return -1;
}
static int ec_drain(ec_t *e)
{
int i;
for (i = 0; i < POLL_LIMIT; i++) {
if (!(ec_status(e) & OBF))
return 0;
ec_in8(e, e->data_port);
nsleep(POLL_DELAY);
}
return ec_timeout("obf never cleared");
}
static int ec_wait_ibf(ec_t *e)
{
int i;
for (i = 0; i < POLL_LIMIT; i++) {
if (!(ec_status(e) & IBF))
return 0;
nsleep(POLL_DELAY);
}
return ec_timeout("ibf never cleared");
}
static int ec_wait_obf(ec_t *e)
{
int i;
for (i = 0; i < POLL_LIMIT; i++) {
if (ec_status(e) & OBF)
return 0;
nsleep(POLL_DELAY);
}
return ec_timeout("obf never set");
}
static int ec_once(ec_t *e, uint8_t cmd, const uint8_t *payload, size_t n,
bool want_result, uint8_t *out)
{
size_t i;
if (ec_drain(e) || ec_wait_ibf(e))
return -1;
ec_out8(e, e->cmd_port, PREAMBLE);
if (ec_wait_ibf(e))
return -1;
ec_out8(e, e->cmd_port, cmd);
for (i = 0; i < n; i++) {
if (ec_wait_ibf(e))
return -1;
ec_out8(e, e->data_port, payload[i]);
}
if (ec_wait_ibf(e))
return -1;
if (!want_result)
return 0;
if (ec_wait_obf(e))
return -1;
if (out)
*out = ec_in8(e, e->data_port);
return 0;
}
int ec_xact(ec_t *e, uint8_t cmd, const uint8_t *payload, size_t n,
bool want_result, uint8_t *out)
{
int i;
if (n > 8)
die("Payload limit is 8 bytes.");
for (i = 0; i < RETRIES; i++)
if (ec_once(e, cmd, payload, n, want_result, out) == 0)
return 0;
return -1;
}
+23
View File
@@ -0,0 +1,23 @@
#ifndef AFC_EC_H
#define AFC_EC_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
typedef struct {
int fd;
bool direct;
unsigned cmd_port;
unsigned data_port;
bool verbose;
} ec_t;
extern char ec_error[64];
int ec_open(ec_t *e, unsigned cmd_port, unsigned data_port, bool verbose);
void ec_close(ec_t *e);
int ec_xact(ec_t *e, uint8_t cmd, const uint8_t *payload, size_t n,
bool want_result, uint8_t *out);
#endif
+109
View File
@@ -0,0 +1,109 @@
#define _GNU_SOURCE
#include "fan.h"
#include "common.h"
#include "util.h"
#include <stdlib.h>
int percent_to_duty(int percent)
{
return (int)(percent * 255 / 100.0 + 0.5);
}
int duty_to_percent(int duty)
{
return (int)(duty * 100 / 255.0 + 0.5);
}
bool duty_close(int readback_value, int want)
{
int slack = want * TOL_PCT / 100;
if (slack < TOL_MIN)
slack = TOL_MIN;
return abs(readback_value - want) <= slack;
}
const char *mode_name(int mode)
{
if (mode < 0)
return "unreadable";
return mode ? "Manual" : "EC automatic";
}
int fan_count_or_default(hy_t *h)
{
uint8_t n;
if (hy_read(h, REG_FAN_COUNT, &n))
return 0;
return (n >= 1 && n <= MAX_FANS) ? n : 0;
}
int apply_duty(hy_t *h, int index, int duty, int order)
{
if (hy_select(h, index))
return -1;
if (order == ORDER_MODE_FIRST) {
if (hy_write(h, REG_TEST_MODE, 1))
return -1;
if (hy_select(h, index))
return -1;
if (hy_write(h, REG_PWM_DUTY, (uint8_t)duty))
return -1;
} else {
if (hy_write(h, REG_PWM_DUTY, (uint8_t)duty))
return -1;
if (hy_write(h, REG_TEST_MODE, 1))
return -1;
}
return 0;
}
int readback(hy_t *h, int index, int *duty, int *mode)
{
uint8_t d, m;
if (hy_select(h, index) || hy_read(h, REG_PWM_DUTY, &d) ||
hy_read(h, REG_TEST_MODE, &m))
return -1;
*duty = d;
*mode = m;
return 0;
}
void hand_back(hy_t *h, int count)
{
int idx;
for (idx = 0; idx < count; idx++) {
if (hy_select(h, idx))
continue;
hy_write(h, REG_TEST_MODE, 0);
}
}
int still_manual(hy_t *h, int count, int *left)
{
int idx, nleft = 0;
for (idx = 0; idx < count; idx++) {
int rb, md;
if (readback(h, idx, &rb, &md) == 0 && md)
left[nleft++] = idx;
}
return nleft;
}
int release_to_ec(hy_t *h, int count)
{
int left[MAX_FANS];
hand_back(h, count);
if (!still_manual(h, count, left))
return APPLY_CLEAN;
hand_back(h, count);
return still_manual(h, count, left) ? APPLY_FAILED : APPLY_RETRIED;
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef AFC_FAN_H
#define AFC_FAN_H
#include "healthy.h"
#include <stdbool.h>
int percent_to_duty(int percent);
int duty_to_percent(int duty);
bool duty_close(int readback_value, int want);
const char *mode_name(int mode);
int fan_count_or_default(hy_t *h);
int apply_duty(hy_t *h, int index, int duty, int order);
int readback(hy_t *h, int index, int *duty, int *mode);
void hand_back(hy_t *h, int count);
int still_manual(hy_t *h, int count, int *left);
int release_to_ec(hy_t *h, int count);
#endif
+45
View File
@@ -0,0 +1,45 @@
#define _GNU_SOURCE
#include "healthy.h"
#include "common.h"
#include "util.h"
int hy_version(hy_t *h, uint8_t *out)
{
uint8_t payload[1] = { 0x50 };
return ec_xact(h->ec, CMD_VERSION, payload, 1, true, out);
}
int hy_read(hy_t *h, uint8_t reg, uint8_t *out)
{
uint8_t payload[3] = { TBL_READ, reg, 0x00 };
return ec_xact(h->ec, CMD_TABLE, payload, 3, true, out);
}
int hy_write(hy_t *h, uint8_t reg, uint8_t value)
{
uint8_t payload[3] = { TBL_WRITE, reg, value };
if (ec_xact(h->ec, CMD_TABLE, payload, 3, false, NULL))
return -1;
nsleep(h->gap);
return 0;
}
int hy_select(hy_t *h, int index)
{
return hy_write(h, REG_FAN_INDEX, (uint8_t)index);
}
int hy_rpm(hy_t *h, int *out)
{
uint8_t hi, lo;
if (hy_read(h, REG_RPM_HI, &hi) || hy_read(h, REG_RPM_LO, &lo))
return -1;
*out = (hi << 8) | lo;
return 0;
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef AFC_HEALTHY_H
#define AFC_HEALTHY_H
#include "ec.h"
#include <stdint.h>
typedef struct {
ec_t *ec;
double gap;
} hy_t;
int hy_version(hy_t *h, uint8_t *out);
int hy_read(hy_t *h, uint8_t reg, uint8_t *out);
int hy_write(hy_t *h, uint8_t reg, uint8_t value);
int hy_select(hy_t *h, int index);
int hy_rpm(hy_t *h, int *out);
#endif
+247
View File
@@ -0,0 +1,247 @@
#define _GNU_SOURCE
#include "commands.h"
#include "common.h"
#include "curve.h"
#include "util.h"
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static void need_root(void)
{
if (geteuid() != 0)
die("Root privileges required.");
}
static const char INT_MSG[] =
"\nInterrupted. Fans keep the last setting, use 'set -1' to hand control "
"back to the EC.\n";
static void on_interrupt(int sig)
{
(void)sig;
if (write(STDOUT_FILENO, INT_MSG, sizeof INT_MSG - 1) < 0) {
}
_exit(130);
}
static void usage_text(FILE *out)
{
fprintf(out,
"usage: " APP_NAME " [global options] <command> [options]\n"
"\n"
"global options:\n"
" --cmd-port N --data-port N --gap SECONDS --verbose\n"
"\n"
"commands:\n"
" set DUTY [--fan N] [--retries N] [--no-verify]\n"
" [--order mode-first|duty-first]\n"
" duty 0-255, or -1 to hand control back to the EC\n"
" -1 always covers every fan, 0x31 is global\n"
" setp PERCENT [same options as set], or -1 for the same release\n"
" curve [--config PATH] [--interval S] [--hysteresis C]\n"
" [--panic-temp C] [--sensor cpu|board|max] [--retries N]\n"
" [--order ORDER] [--once] [--dry-run] [--silent]\n"
" config lines are 'percent,temp'; percent -1 hands that\n"
" band back to the EC\n"
" fan-speed\n"
" fan-info\n"
" temps-info\n"
" help\n"
" version\n");
}
static void usage(void)
{
usage_text(stderr);
exit(2);
}
static int cmd_help(void)
{
usage_text(stdout);
return 0;
}
static const char *need_value(int argc, char **argv, int *i)
{
if (*i + 1 >= argc)
die("%s requires a value.", argv[*i]);
return argv[++(*i)];
}
static long int_auto(const char *s, const char *what)
{
char *end;
long v;
errno = 0;
v = strtol(s, &end, 0);
if (errno || end == s || *end)
die("Invalid %s: %s.", what, s);
return v;
}
static double dbl_arg(const char *s, const char *what)
{
char *end;
double v;
errno = 0;
v = strtod(s, &end);
if (errno || end == s || *end)
die("invalid %s: %s", what, s);
return v;
}
static int pick(const char *value, const char *what, const char *const *names,
int n)
{
int i;
for (i = 0; i < n; i++)
if (strcmp(value, names[i]) == 0)
return i;
die("Invalid %s: %s.", what, value);
}
static bool global_option(args_t *a, int argc, char **argv, int *i)
{
const char *arg = argv[*i];
if (strcmp(arg, "--cmd-port") == 0)
a->cmd_port = (unsigned)int_auto(need_value(argc, argv, i), "--cmd-port");
else if (strcmp(arg, "--data-port") == 0)
a->data_port = (unsigned)int_auto(need_value(argc, argv, i), "--data-port");
else if (strcmp(arg, "--gap") == 0)
a->gap = dbl_arg(need_value(argc, argv, i), "--gap");
else if (strcmp(arg, "--verbose") == 0)
a->verbose = true;
else
return false;
return true;
}
int main(int argc, char **argv)
{
static const char *const orders[] = { "mode-first", "duty-first" };
static const char *const sensors[] = { "cpu", "board", "max" };
args_t a;
struct sigaction sa;
const char *cmd = NULL;
bool have_duty = false;
int i;
for (i = 1; i < argc; i++)
if (strcmp(argv[i], "help") == 0 || strcmp(argv[i], "--help") == 0 ||
strcmp(argv[i], "-h") == 0)
return cmd_help();
memset(&a, 0, sizeof a);
a.cmd_port = CMD_PORT;
a.data_port = DATA_PORT;
a.gap = IO_GAP;
a.index = -1;
a.retries = 3;
a.order = ORDER_MODE_FIRST;
a.config = DEFAULT_CONFIG;
a.interval = INTERVAL;
a.hysteresis = HYSTERESIS;
a.panic_temp = PANIC_TEMP;
a.sensor = SENSOR_MAX;
i = 1;
while (i < argc && argv[i][0] == '-' && argv[i][1]) {
if (!global_option(&a, argc, argv, &i))
usage();
i++;
}
if (i >= argc)
usage();
cmd = argv[i++];
if (strcmp(cmd, "version") != 0)
need_root();
for (; i < argc; i++) {
const char *arg = argv[i];
if (global_option(&a, argc, argv, &i))
continue;
if (arg[0] != '-' || isdigit((unsigned char)arg[1])) {
if (strcmp(cmd, "set") == 0 && !have_duty) {
a.duty = (int)int_auto(arg, "duty");
have_duty = true;
} else if (strcmp(cmd, "setp") == 0 && !have_duty) {
a.percent = (int)int_auto(arg, "percent");
have_duty = true;
} else {
usage();
}
continue;
}
if (strcmp(arg, "--fan") == 0)
a.index = (int)int_auto(need_value(argc, argv, &i), "--fan");
else if (strcmp(arg, "--retries") == 0)
a.retries = (int)int_auto(need_value(argc, argv, &i), "--retries");
else if (strcmp(arg, "--no-verify") == 0)
a.no_verify = true;
else if (strcmp(arg, "--order") == 0)
a.order = pick(need_value(argc, argv, &i), "--order", orders, 2);
else if (strcmp(arg, "--sensor") == 0)
a.sensor = pick(need_value(argc, argv, &i), "--sensor", sensors, 3);
else if (strcmp(arg, "--config") == 0)
a.config = need_value(argc, argv, &i);
else if (strcmp(arg, "--interval") == 0)
a.interval = dbl_arg(need_value(argc, argv, &i), "--interval");
else if (strcmp(arg, "--hysteresis") == 0)
a.hysteresis = (int)int_auto(need_value(argc, argv, &i), "--hysteresis");
else if (strcmp(arg, "--panic-temp") == 0)
a.panic_temp = (int)int_auto(need_value(argc, argv, &i), "--panic-temp");
else if (strcmp(arg, "--once") == 0)
a.once = true;
else if (strcmp(arg, "--dry-run") == 0)
a.dry_run = true;
else if (strcmp(arg, "--silent") == 0)
a.silent = true;
else {
usage();
}
}
if (strcmp(cmd, "set") == 0 && !have_duty)
die("set requires a duty value.");
if (strcmp(cmd, "setp") == 0 && !have_duty)
die("setp requires a percent value.");
if (a.index >= MAX_FANS)
die("--fan must be 0..%d.", MAX_FANS - 1);
if (a.retries < 1)
die("--retries must be at least 1.");
memset(&sa, 0, sizeof sa);
sa.sa_handler = on_interrupt;
sigaction(SIGINT, &sa, NULL);
if (strcmp(cmd, "version") == 0)
return cmd_version(&a);
if (strcmp(cmd, "fan-info") == 0)
return cmd_fan_info(&a);
if (strcmp(cmd, "temps-info") == 0)
return cmd_temps_info(&a);
if (strcmp(cmd, "fan-speed") == 0)
return cmd_fan_speed(&a);
if (strcmp(cmd, "set") == 0)
return cmd_set(&a);
if (strcmp(cmd, "setp") == 0)
return cmd_setp(&a);
if (strcmp(cmd, "curve") == 0)
return cmd_curve(&a);
usage();
return 2;
}
+86
View File
@@ -0,0 +1,86 @@
#define _GNU_SOURCE
#define _FILE_OFFSET_BITS 64
#include "mmio.h"
#include "common.h"
#include "util.h"
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
void mmio_open(mmio_t *m, unsigned long base)
{
void *p;
m->fd = open("/dev/mem", O_RDONLY | O_SYNC);
if (m->fd < 0)
die("Cannot open /dev/mem: %s.", strerror(errno));
m->page = base & PAGE_MASK;
m->off = base - m->page;
p = mmap(NULL, 0x1000, PROT_READ, MAP_SHARED, m->fd, (off_t)m->page);
if (p == MAP_FAILED)
die("Cannot map 0x%lX: %s.", m->page, strerror(errno));
m->map = p;
}
void mmio_close(mmio_t *m)
{
if (m->map)
munmap((void *)m->map, 0x1000);
if (m->fd >= 0)
close(m->fd);
m->map = NULL;
m->fd = -1;
}
int mmio_be16(mmio_t *m, unsigned long offset)
{
unsigned long i = m->off + offset;
return (m->map[i] << 8) | m->map[i + 1];
}
void mmio_fans(mmio_t *m, int fans[2])
{
fans[0] = mmio_be16(m, TACH_OFFSET);
fans[1] = mmio_be16(m, TACH_OFFSET + 2);
}
int mmio_at(mmio_t *m, unsigned long address)
{
return m->map[address - m->page];
}
void mmio_temps(mmio_t *m, int *cpu, int *board)
{
*cpu = mmio_at(m, CTMP);
*board = mmio_at(m, CLOT);
}
const char *sensor_name(int sensor)
{
switch (sensor) {
case SENSOR_CPU:
return "cpu";
case SENSOR_BOARD:
return "board";
default:
return "max";
}
}
int sensor_pick(int sensor, int cpu, int board)
{
switch (sensor) {
case SENSOR_CPU:
return cpu;
case SENSOR_BOARD:
return board;
default:
return imax(cpu, board);
}
}
+20
View File
@@ -0,0 +1,20 @@
#ifndef AFC_MMIO_H
#define AFC_MMIO_H
typedef struct {
int fd;
volatile unsigned char *map;
unsigned long page;
unsigned long off;
} mmio_t;
void mmio_open(mmio_t *m, unsigned long base);
void mmio_close(mmio_t *m);
int mmio_be16(mmio_t *m, unsigned long offset);
int mmio_at(mmio_t *m, unsigned long address);
void mmio_fans(mmio_t *m, int fans[2]);
void mmio_temps(mmio_t *m, int *cpu, int *board);
const char *sensor_name(int sensor);
int sensor_pick(int sensor, int cpu, int board);
#endif
+62
View File
@@ -0,0 +1,62 @@
#define _GNU_SOURCE
#include "util.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
void die(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
exit(1);
}
void nsleep(double seconds)
{
struct timespec ts;
if (seconds <= 0.0)
return;
ts.tv_sec = (time_t)seconds;
ts.tv_nsec = (long)((seconds - (double)ts.tv_sec) * 1e9);
if (ts.tv_nsec < 0)
ts.tv_nsec = 0;
if (ts.tv_nsec > 999999999L)
ts.tv_nsec = 999999999L;
nanosleep(&ts, NULL);
}
bool read_line_file(const char *path, char *buf, size_t size)
{
FILE *f = fopen(path, "r");
size_t len;
if (!f)
return false;
if (!fgets(buf, (int)size, f)) {
fclose(f);
return false;
}
fclose(f);
len = strlen(buf);
while (len && (buf[len - 1] == '\n' || buf[len - 1] == '\r'))
buf[--len] = '\0';
return len > 0;
}
void join_ints(char *buf, size_t size, const int *v, int n)
{
int i;
size_t used = 0;
buf[0] = '\0';
for (i = 0; i < n && used + 8 < size; i++)
used += (size_t)snprintf(buf + used, size - used, "%s%d", i ? " " : "", v[i]);
}
+15
View File
@@ -0,0 +1,15 @@
#ifndef AFC_UTIL_H
#define AFC_UTIL_H
#include <stdbool.h>
#include <stddef.h>
void die(const char *fmt, ...) __attribute__((noreturn, format(printf, 1, 2)));
void nsleep(double seconds);
bool read_line_file(const char *path, char *buf, size_t size);
void join_ints(char *buf, size_t size, const int *v, int n);
static inline int imin(int a, int b) { return a < b ? a : b; }
static inline int imax(int a, int b) { return a > b ? a : b; }
#endif