x86_rtc.c 8.89 KB
/*
 * Copyright (C) 2014  René Kijewski  <rene.kijewski@fu-berlin.de>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

/**
 * @ingroup     x86-irq
 * @{
 *
 * @file
 * @brief       Reading and interrupt handling for the Real Time Clock (RTC).
 *
 * @author      René Kijewski <rene.kijewski@fu-berlin.de>
 *
 * @}
 */

#include "x86_pic.h"
#include "x86_rtc.h"
#include "irq.h"

#include <stdio.h>


#define ENABLE_DEBUG (0)
#include "debug.h"

static bool valid;

static int32_t alarm_msg_content, periodic_msg_content, update_msg_content;
static kernel_pid_t alarm_pid = KERNEL_PID_UNDEF, periodic_pid = KERNEL_PID_UNDEF, update_pid = KERNEL_PID_UNDEF;

static void alarm_callback_default(uint8_t reg_c)
{
    if (alarm_pid != KERNEL_PID_UNDEF) {
        msg_t m;
        m.type = reg_c | (RTC_REG_B_INT_ALARM << 8);
        m.content.value = alarm_msg_content;
        msg_send_int(&m, alarm_pid);
    }
}

static void periodic_callback_default(uint8_t reg_c)
{
    if (periodic_pid != KERNEL_PID_UNDEF) {
        msg_t m;
        m.type = reg_c | (RTC_REG_B_INT_PERIODIC << 8);
        m.content.value = periodic_msg_content;
        msg_send_int(&m, periodic_pid);
    }
}

static void update_callback_default(uint8_t reg_c)
{
    if (update_pid != KERNEL_PID_UNDEF) {
        msg_t m;
        m.type = reg_c | (RTC_REG_B_INT_UPDATE << 8);
        m.content.value = update_msg_content;
        msg_send_int(&m, update_pid);
    }
}

static x86_rtc_callback_t alarm_callback = alarm_callback_default;
static x86_rtc_callback_t periodic_callback = periodic_callback_default;
static x86_rtc_callback_t update_callback = update_callback_default;

void x86_rtc_set_alarm_callback(x86_rtc_callback_t cb)
{
    alarm_callback = cb ? cb : alarm_callback_default;
}

void x86_rtc_set_periodic_callback(x86_rtc_callback_t cb)
{
    periodic_callback = cb ? cb : periodic_callback_default;
}

void x86_rtc_set_update_callback(x86_rtc_callback_t cb)
{
    update_callback = cb ? cb : update_callback_default;
}

static void rtc_irq_handler(uint8_t irq_num)
{
    (void) irq_num; /* == PIC_NUM_RTC */

    uint8_t c = x86_cmos_read(RTC_REG_C);
    DEBUG("RTC: c = 0x%02x, IRQ=%u, A=%u, P=%u, U=%u\n", c, (c & RTC_REG_C_IRQ) ? 1 : 0,
                                                            (c & RTC_REG_C_IRQ_ALARM) ? 1 : 0,
                                                            (c & RTC_REG_C_IRQ_PERIODIC) ? 1 : 0,
                                                            (c & RTC_REG_C_IRQ_UPDATE) ? 1 : 0);
    if (!(c & RTC_REG_C_IRQ)) {
        return;
    }
    if (c & RTC_REG_C_IRQ_ALARM) {
        alarm_callback(c);
    }
    if (c & RTC_REG_C_IRQ_PERIODIC) {
        periodic_callback(c);
    }
    if (c & RTC_REG_C_IRQ_UPDATE) {
        update_callback(c);
    }
}

void x86_init_rtc(void)
{
    uint8_t d = x86_cmos_read(RTC_REG_D);
    valid = (d & RTC_REG_D_VALID) != 0;
    if (!valid) {
        puts("Warning: RTC does not work.");
        return;
    }

    x86_cmos_write(RTC_REG_B, x86_cmos_read(RTC_REG_B) & ~RTC_REG_B_INT_MASK);
    rtc_irq_handler(0);
    x86_pic_set_handler(PIC_NUM_RTC, &rtc_irq_handler);
    x86_pic_enable_irq(PIC_NUM_RTC);

    x86_rtc_data_t now;
    x86_rtc_read(&now);
    printf("RTC initialized [%02hhu:%02hhu:%02hhu, %04u-%02hhu-%02hhu]\n",
           now.hour, now.minute, now.second,
           now.century * 100 + now.year, now.month, now.day);

    if (x86_cmos_read(RTC_REG_POST) & (RTC_REG_POST_POWER_LOSS | RTC_REG_POST_TIME_INVALID)) {
        puts("Warning: RTC time is invalid (power loss?)");
    }
}

static inline bool is_update_in_progress(void)
{
    return (x86_cmos_read(RTC_REG_A) & RTC_REG_A_UPDATING) != 0;
}

static uint8_t bcd2binary(uint8_t datum)
{
    return (datum / 16) * 10 + (datum % 16);
}

static uint8_t binary2bcd(uint8_t datum)
{
    return (datum / 10) * 16 + (datum % 10);
}

bool x86_rtc_read(x86_rtc_data_t *dest)
{
    if (!valid) {
        return false;
    }

    unsigned old_status = irq_disable();

    while (is_update_in_progress()) {
        __asm__ volatile ("pause");
    }

    uint8_t b = x86_cmos_read(RTC_REG_B);
    do {
        dest->second  = x86_cmos_read(RTC_REG_SECOND);
        dest->minute  = x86_cmos_read(RTC_REG_MINUTE);
        dest->hour    = x86_cmos_read(RTC_REG_HOUR);
        dest->day     = x86_cmos_read(RTC_REG_DAY);
        dest->month   = x86_cmos_read(RTC_REG_MONTH);
        dest->year    = x86_cmos_read(RTC_REG_YEAR);
        dest->century = bcd2binary(x86_cmos_read(RTC_REG_CENTURY));
    } while (dest->second != x86_cmos_read(RTC_REG_SECOND));
    if (dest->century == 0) {
        dest->century = 20; // safe guess
    }

    if (!(b & RTC_REG_B_BIN)) {
        dest->second  = bcd2binary(dest->second);
        dest->minute  = bcd2binary(dest->minute);
        dest->hour    = ((dest->hour & 0x0F) + (((dest->hour & 0x70) / 16) * 10)) | (dest->hour & 0x80);
        dest->day     = bcd2binary(dest->day);
        dest->month   = bcd2binary(dest->month);
        dest->year    = bcd2binary(dest->year);
    }
    if (!(b & RTC_REG_B_24H) && (dest->hour & 0x80)) {
        dest->hour = ((dest->hour & 0x7F) + 12) % 24;
    }

    irq_restore(old_status);
    return true;
}

bool x86_rtc_set_alarm(const x86_rtc_data_t *when, uint32_t msg_content, kernel_pid_t target_pid, bool allow_replace)
{
    if (!valid) {
        return false;
    }

    unsigned old_status = irq_disable();
    bool result;
    if (target_pid == KERNEL_PID_UNDEF) {
        result = true;
        alarm_pid = KERNEL_PID_UNDEF;

        uint8_t b = x86_cmos_read(RTC_REG_B);
        x86_cmos_write(RTC_REG_B, b & ~RTC_REG_B_INT_ALARM);
    }
    else {
        result = allow_replace || alarm_pid == KERNEL_PID_UNDEF;
        if (result) {
            alarm_msg_content = msg_content;
            alarm_pid = target_pid;

            uint8_t b = x86_cmos_read(RTC_REG_B);
            if (b & RTC_REG_B_BIN) {
                x86_cmos_write(RTC_REG_ALARM_SECOND, when->second);
                x86_cmos_write(RTC_REG_ALARM_MINUTE, when->minute);
                x86_cmos_write(RTC_REG_ALARM_HOUR, when->hour);
            }
            else {
                x86_cmos_write(RTC_REG_ALARM_SECOND, binary2bcd(when->second));
                x86_cmos_write(RTC_REG_ALARM_MINUTE, binary2bcd(when->minute));
                x86_cmos_write(RTC_REG_ALARM_HOUR, binary2bcd(when->hour));
            }
            x86_cmos_write(RTC_REG_B, b | RTC_REG_B_INT_ALARM);
        }
    }
    rtc_irq_handler(0);
    irq_restore(old_status);
    return result;
}

bool x86_rtc_set_periodic(uint8_t hz, uint32_t msg_content, kernel_pid_t target_pid, bool allow_replace)
{
    if (!valid) {
        return false;
    }

    unsigned old_status = irq_disable();
    bool result;
    if (target_pid == KERNEL_PID_UNDEF || hz == RTC_REG_A_HZ_OFF) {
        result = true;
        periodic_pid = KERNEL_PID_UNDEF;

        uint8_t old_divider = x86_cmos_read(RTC_REG_A) & ~RTC_REG_A_HZ_MASK;
        x86_cmos_write(RTC_REG_A, old_divider | RTC_REG_A_HZ_OFF);
        x86_cmos_write(RTC_REG_B, x86_cmos_read(RTC_REG_B) & ~RTC_REG_B_INT_PERIODIC);
    }
    else {
        result = allow_replace || periodic_pid == KERNEL_PID_UNDEF;
        if (result) {
            periodic_msg_content = msg_content;
            periodic_pid = target_pid;

            uint8_t old_divider = x86_cmos_read(RTC_REG_A) & ~RTC_REG_A_HZ_MASK;
            x86_cmos_write(RTC_REG_A, old_divider | hz);
            x86_cmos_write(RTC_REG_B, x86_cmos_read(RTC_REG_B) | RTC_REG_B_INT_PERIODIC);
        }
    }
    rtc_irq_handler(0);
    irq_restore(old_status);
    return result;
}

bool x86_rtc_set_update(uint32_t msg_content, kernel_pid_t target_pid, bool allow_replace)
{
    if (!valid) {
        return false;
    }

    unsigned old_status = irq_disable();
    bool result;
    if (target_pid == KERNEL_PID_UNDEF) {
        result = true;
        update_pid = KERNEL_PID_UNDEF;

        x86_cmos_write(RTC_REG_B, x86_cmos_read(RTC_REG_B) & ~RTC_REG_B_INT_UPDATE);
    }
    else {
        result = allow_replace || update_pid == KERNEL_PID_UNDEF;
        if (result) {
            update_msg_content = msg_content;
            update_pid = target_pid;

            x86_cmos_write(RTC_REG_B, x86_cmos_read(RTC_REG_B) | RTC_REG_B_INT_UPDATE);
        }
    }
    rtc_irq_handler(0);
    irq_restore(old_status);
    return result;
}