8位單片機(如8051、STM8、AVR)資源受限,標準C庫 time.h 往往缺失或體積過大。本文提供一套經過驗證的 mktime(結構體轉時間戳)與 localtime(時間戳轉結構體)純C實現,無操作系統依賴,適配 8 位 MCU 有限 RAM/ROM,並通過 GCC 與 Keil C51 雙環境測試,結果與標準庫完全一致(僅時區差異)。
mktime/localtime 完全一致stdint.h、string.h,無平台相關代碼// 標準 tm 結構體(精簡版)
struct tm {
int tm_sec; // 0-59
int tm_min; // 0-59
int tm_hour; // 0-23
int tm_mday; // 1-31
int tm_mon; // 0-11 (月份-1)
int tm_year; // 年份-1900
// 以下字段本實現不使用,但保留兼容性
// int tm_wday; // 0-6 星期
// int tm_yday; // 0-365 年內天數
// int tm_isdst; // 夏令時標志
};
// 平年每月天數
static const char mon_days[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
// 閏年每月天數
static const char leap_mon_days[12] = {31,29,31,30,31,30,31,31,30,31,30,31};
// 年判斷宏
#define IS_LEAP(y) (((y)%4==0 && (y)%100!=0) || (y)%400==0)
int32_t fun_mktime(struct tm* pt) {
const char* pDays = NULL;
int32_t days = 0;
int16_t i;
// 1. 計算 1970 年至 pt->tm_year 年的總天數
// 利用數學公式快速計算閏年數:y/4 - y/100 + y/400
int32_t leap_cnt = (pt->tm_year/4 - pt->tm_year/100 + pt->tm_year/400)
- (1970/4 - 1970/100 + 1970/400);
// 2. 當年是否閏年,決定月表與當年天數
if (IS_LEAP(pt->tm_year)) {
days = leap_cnt - 1 + (pt->tm_year - 1970) * 365; // -1: 當年閏日尚未到來
pDays = leap_mon_days;
} else {
days = leap_cnt + (pt->tm_year - 1970) * 365;
pDays = mon_days;
}
// 3. 累加當年已過月份天數
for (i = 0; i < pt->tm_mon - 1; i++) days += pDays[i];
// 4. 加上當月已過天數(tm_mday 從1開始)
days += pt->tm_mday - 1;
// 5. 天→時→分→秒 展開
days = days * 24 + pt->tm_hour;
days = days * 60 + pt->tm_min;
days = days * 60 + pt->tm_sec;
return days; // 秒數時間戳
}
void fun_localtime(struct tm* pt, int32_t timestamp) {
const char* pDays = NULL;
uint16_t idx = 0;
memset(pt, 0, sizeof(*pt));
// 1. 年份快速定位(分段減法,避免逐年循環)
if (timestamp >= 0x5685C180L) { // 2016-01-01 00:00:00
pt->tm_year = 2016; timestamp -= 0x5685C180L;
} else if (timestamp >= 0x4B3D3B00L) { // 2010-01-01
pt->tm_year = 2010; timestamp -= 0x4B3D3B00L;
} else if (timestamp >= 0x386D4380L) { // 2000-01-01
pt->tm_year = 2000; timestamp -= 0x386D4380L;
} else {
pt->tm_year = 1970;
}
// 2. 逐年減法精確定位年份
while (timestamp >= 366L * 86400L) {
if (IS_LEAP(pt->tm_year)) timestamp -= 366L * 86400L;
else timestamp -= 365L * 86400L;
pt->tm_year++;
}
// 處理平年邊界:非閏年但剩餘秒數超過365天
if (!IS_LEAP(pt->tm_year) && timestamp >= 365L * 86400L) {
timestamp -= 365L * 86400L;
pt->tm_year++;
}
// 3. 確定月表
pDays = IS_LEAP(pt->tm_year) ? leap_mon_days : mon_days;
pt->tm_mon = 1;
// 4. 逐月減法定位月份
while (timestamp >= (int32_t)pDays[idx] * 86400L) {
timestamp -= (int32_t)pDays[idx] * 86400L;
idx++;
pt->tm_mon++;
}
// 5. 秒數分解:天→時→分→秒
pt->tm_mday = timestamp / 86400L + 1;
timestamp %= 86400L;
pt->tm_hour = timestamp / 3600L;
timestamp %= 3600L;
pt->tm_min = timestamp / 60L;
pt->tm_sec = timestamp % 60L;
}
int main() {
struct tm t = {0};
time_t now; time(&now);
struct tm* std = localtime(&now);
// 標準庫結果(含時區)
printf("linux time = %ld\n", now);
std->tm_year += 1900; std->tm_mon += 1;
printf("localtime = %d-%d-%d %d:%d:%d\n",
std->tm_year, std->tm_mon, std->tm_mday,
std->tm_hour, std->tm_min, std->tm_sec);
// 本實現結果(UTC,無時區)
int32_t stamp = fun_mktime(std);
printf("fun_mktime = %ld\n", stamp);
fun_localtime(&t, stamp);
printf("fun_localtime = %d-%d-%d %d:%d:%d\n",
t.tm_year, t.tm_mon, t.tm_mday,
t.tm_hour, t.tm_min, t.tm_sec);
return 0;
}
linux time = 1480133002 fun_mktime = 1480161802 localtime = 2016-11-26 12:03:22 fun_localtime = 2016-11-26 12:03:22
差值分析:1480161802 - 1480133002 = 28800 = 8×3600 秒,恰好為東八區時區偏移。本實現輸出 UTC 時間,標準庫 localtime 輸出本地時間,邏輯完全一致。
int32_t → long,uint16_t → unsigned intmemset 來自 string.h,Keil 需確認庫支持86400L 確保長整數運算,防 16 位溢出本實現通過數學公式加速年份計算、查表法處理月長、分段減法避免大循環,在 8 位單片機資源極其受限的環境下,高效實現了標準級時間轉換功能。代碼經 GCC x86_64 與 Keil C51 實測驗證,邏輯正確、邊界清晰、移植簡單,可直接集成到嵌入式項目中替代缺失的 time.h 庫函數。