理解ext4,第二部分时间戳(Timestamps)

本文深入探讨了EXT4文件系统如何处理文件时间戳,包括访问时间(atime)、修改时间(mtime)和变化时间(ctime),并详细解释了EXT4如何通过64位时间戳和扩展部分解决2038年问题,同时对比了不同命令对时间戳的显示方式。文章还展示了如何在EXT4中设置和查看文件时间戳。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

准备环境

首先,在EXT4文件系统下,创建另一个测试文件

# echo Time for knowledge >testfile
# touch -a -t 211101231917.42 testfile
# touch -m -t 204005160308.19 testfile
使用touch命令可以直接修改文件的atime(最后访问时间)、mtime(最后修改时间),这样可以把这些时间戳设置为我们想要的时间,否则这个新创建文件的atime、mtime时间戳将会是创建文件的时间。有一点需要指出,我刚才设置的atime、mtime都是遥远的未来的一个时间点,而在旧的Unix的文件系统中,由于时间信息是32位的,所以会引起一些麻烦(比如“ 2038年问题”,或称unix千年虫)。

现在,让我们比较一下标准的linux下的stat命令、Sleuthkit工具包里的istat、debugfs版本的stat的输出结果:

# stat testfile
 File: `testfile'
 Size: 19  Blocks: 8 IO Block: 4096 regular file
Device: fc03h/64515d Inode: 6554914 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2111-01-23 19:17:42.000000000 -0800
Modify: 2040-05-16 03:08:19.000000000 -0700
Change: 2011-03-12 07:36:13.872411014 -0800
# istat /dev/mapper/RD-home 6554914
inode: 6554914
Allocated
Group: 800
[...]
Inode Times:
Accessed:       Tue Dec 17 12:49:26 1974
File Modified:  Wed May 16 03:08:19 2040
Inode Modified: Sat Mar 12 07:36:13 2011
[...]
# debugfs -R 'stat ' /dev/mapper/RD-home
[...]
 ctime: 0x4d7b92ed:cfffbe18 -- Sat Mar 12 07:36:13 2011
 atime: 0x0954b156:00000001 -- Tue Dec 17 12:49:26 1974
 mtime: 0x845e5913:00000000 -- Wed May 16 03:08:19 2040
crtime: 0x4d7b92e4:148af06c -- Sat Mar 12 07:36:04 2011
[...]
我已经对输出进行了编辑,使时间戳比较明显。正如你所看到的,这三个命令输出了相同的mtime和ctime,但是istat和debugfs在解析2111年的atime的时候出现了问题,只有stat命令获得了正确值。

你可能已经也注意到了EXT4的一些新功能:

  • stat命令、debugfs命令输出的时间戳带有小数部分,现在EXT4时间戳支持精确到毫微秒(nanosecond)。
  • 在debugfs的stat中可以看到"crtime"(create time)这一项,在EXT4中终于支持文件“诞生”的时间戳,就像NTFS一样,用来标记文件被创建的时间点。
  • 至少在stat、touch命令已经可以完美的支持未来的时间戳。EXT4的实现方式已经解决了(事实上是推迟了)2038年问题。

EXT4通过64位的时间戳来提供这些功能,这给你足够的空间来支持毫微秒级的时间戳,并且还有空余的两位可以满足未来对时间戳的扩展要求。


显示细节

但是,就像我提到的,EXT4的开发者尽了最大的努力,使EXT4能够向下兼容EXT2、EXT3的inode结构。64位的时间戳、全新的文件创建时间项,明显与这个目标相矛盾。EXT4开发者解决这个问题的方法是通过在EXT4的256位inode中的高128位中,设置一个扩展部分。

这是一个16位编辑器显示的结果(请参照第一部分):


将一个文件inode的开始作为0,则各个高亮的时间戳的区域如下所示:

Bytes   8 -  11: Access time seconds
       12 -  15: Change time seconds
       16 -  19: Modification time seconds

132 - 135: Change time "extra" 136 - 139: Modification time "extra" 140 - 143: Access time "extra" 144 - 147: Create time seconds 148 - 151: Create time "extra"

在inode起始位置的标准MACtime值本质上没有改变。它们以自1970年1月1日(unix元年)以来的秒数,然后,EXT4将这些值以无符号数而不是有符号数的形式保存。使用扩展位的方式使“2038年问题”变为“2106年问题”。

你可以实际看到上面例子中的2040年的mtime。通过小端字节序传换而来,16进制的mtime值是0x845E5913, 或者十进制的2220775699,这个数将会引起32位有符号数产生溢出。可以通过以下命令将这个十进制数转换为可读的日期:

# date -d @2220775699
Wed May 16 03:08:19 PDT 2040




classdef ECG_Monitor_System < handle properties % 通信相关 serialPort % 串口对象 isConnected = false comPort = 'COM1' % 默认串口 baudRate = 115200 % OEM协议常用波特率 dataBits = 8 % 数据位 parity = 'none' % 校验位 stopBits = 1 % 停止位 % 协议栈属性 frameBuffer = []; % 帧缓冲区 inFrame = false; % 帧接收状态标志 frameLength = 0; % 当前帧长度 expectedLength = 8; % CAN标准帧长度 % 会话管理 currentSession = 0; % 当前会话状态(0=默认,1=扩展诊断) securityLevel = 0; % 安全访问级别(0=未授权,1=编程,2=安全) % 数据管理 ecgData = zeros(12, 10000); filteredData = zeros(12, 10000); timeStamps = zeros(1, 10000); sampleRate = 1000; currentIndex = 1; dataLength = 0; isRecording = false; timerObj maxDataPoints = 10000 % 最大数据点数 numLeads = 12 % 导联数量 activeLead = 2 % 默认显示II导联 % 滤波参数 highPassFreq = 0.5; lowPassFreq = 100; notchFreq = 50; % 工频陷波 % 分析参数 heartRate = 0; rPeakPositions = []; qtInterval = 0; stSegment = 0; % 心电特征参数 qtIntervals = [] % QT间期数组() stSegments = [] % ST段偏移数组(mV) avgQT = 0 % 平均QT间期 avgST = 0 % 平均ST偏移 % GUI组件 fig % 主控制窗口 overviewFig % 总览窗口 singleLeadFigs = gobjects(12, 1); % 单导联窗口 leadPlots = gobjects(12, 1); % 单导联绘图句柄 overviewPlots = gobjects(12, 1); % 总览窗口绘图句柄 leadNames = {'I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6'}; % 控制面板组件 controlPanel highPassEdit lowPassEdit notchCheckbox statusText heartRateText recordButton saveButton leadSelect dataSlider positionText % 频谱分析 spectrumAx % 报告生成 patientInfo = struct('name', '未知', 'gender', '未知', 'age', 0, 'id', ''); end methods function app = ECG_Monitor_System() app.createMainGUI(); app.createOverviewWindow(); app.initializeSystem(); end function createMainGUI(app) % 创建主控制窗口 app.fig = figure('Name', '心电图监测系统 - 控制面板', 'NumberTitle', 'off',... 'Position', [100, 100, 800, 600], 'CloseRequestFcn', @app.onClose,... 'MenuBar', 'none', 'ToolBar', 'none'); % 创建菜单栏 app.createMenus(); % 创建控制面板 app.createControlPanel(); end function createOverviewWindow(app) % 创建总览窗口 app.overviewFig = figure('Name', '心电图总览', 'NumberTitle', 'off',... 'Position', [950, 100, 1200, 800], 'CloseRequestFcn', @app.onOverviewClose); % 创建12导联网格布局 for i = 1:12 row = ceil(i/3); col = mod(i-1,3)+1; ax = subplot(4, 3, i, 'Parent', app.overviewFig); app.overviewPlots(i) = plot(ax, nan, nan, 'b-'); title(ax, app.leadNames{i}); xlabel(ax, '时间(s)'); ylabel(ax, '幅度(mV)'); grid(ax, 'on'); end end function createSingleLeadWindow(app, leadIndex) % 创建单导联窗口 if ishandle(app.singleLeadFigs(leadIndex)) figure(app.singleLeadFigs(leadIndex)); % 激活现有窗口 return; end figName = sprintf('导联 %s 详细视图', app.leadNames{leadIndex}); app.singleLeadFigs(leadIndex) = figure('Name', figName, 'NumberTitle', 'off',... 'Position', [100 + 50*leadIndex, 100 + 50*leadIndex, 600, 400],... 'CloseRequestFcn', @(src,evt) app.onSingleLeadClose(leadIndex)); ax = axes('Parent', app.singleLeadFigs(leadIndex), 'Position', [0.1, 0.15, 0.85, 0.75]); app.leadPlots(leadIndex) = plot(ax, nan, nan, 'b-'); title(ax, sprintf('导联 %s - 详细视图', app.leadNames{leadIndex})); xlabel(ax, '时间(s)'); ylabel(ax, '幅度(mV)'); grid(ax, 'on'); % 添加导联特定控制 uicontrol('Parent', app.singleLeadFigs(leadIndex), 'Style', 'pushbutton',... 'String', '检测R波', 'Position', [20, 10, 80, 25],... 'Callback', @(src,evt) app.detectRPeaksForLead(leadIndex)); uicontrol('Parent', app.singleLeadFigs(leadIndex), 'Style', 'pushbutton',... 'String', '频谱分析', 'Position', [120, 10, 80, 25],... 'Callback', @(src,evt) app.showSpectrumForLead(leadIndex)); end function createMenus(app) % 文件菜单 fileMenu = uimenu(app.fig, 'Text', '文件'); uimenu(fileMenu, 'Text', '加载数据', 'MenuSelectedFcn', @app.loadDataFile); uimenu(fileMenu, 'Text', '保存数据', 'MenuSelectedFcn', @app.saveData); uimenu(fileMenu, 'Text', '生成报告', 'MenuSelectedFcn', @app.generateReport); uimenu(fileMenu, 'Text', '退出', 'Separator', 'on', 'MenuSelectedFcn', @app.onClose); % 通信菜单 comMenu = uimenu(app.fig, 'Text', '通信'); uimenu(comMenu, 'Text', '连接设备', 'MenuSelectedFcn', @app.connectDevice); uimenu(comMenu, 'Text', '断开连接', 'MenuSelectedFcn', @app.disconnect); uimenu(comMenu, 'Text', '启动诊断会话', 'MenuSelectedFcn', @(src,evt) app.startSession(1)); % 视图菜单 viewMenu = uimenu(app.fig, 'Text', '视图'); uimenu(viewMenu, 'Text', '显示总览窗口', 'MenuSelectedFcn', @app.showOverviewWindow); uimenu(viewMenu, 'Text', '显示所有导联窗口', 'MenuSelectedFcn', @app.showAllLeadWindows); uimenu(viewMenu, 'Text', '关闭所有导联窗口', 'MenuSelectedFcn', @app.closeAllLeadWindows); % 设置菜单 settingsMenu = uimenu(app.fig, 'Text', '设置'); uimenu(settingsMenu, 'Text', '通信设置', 'MenuSelectedFcn', @app.setCommunication); uimenu(settingsMenu, 'Text', '患者信息', 'MenuSelectedFcn', @app.setPatientInfo); uimenu(settingsMenu, 'Text', '分析参数', 'MenuSelectedFcn', @app.setAnalysisParams); end function createControlPanel(app) % 创建控制面板 app.controlPanel = uipanel('Parent', app.fig, 'Title', '控制面板',... 'Position', [0.05, 0.05, 0.9, 0.9]); % 通信控制 uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '连接设备', 'Position', [20, 520, 100, 30],... 'Callback', @app.connectDevice); uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '断开连接', 'Position', [140, 520, 100, 30],... 'Callback', @app.disconnect); app.statusText = uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '状态: 未连接', 'Position', [260, 520, 300, 30],... 'HorizontalAlignment', 'left', 'FontSize', 10); % 会话状态 uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '会话: 默认', 'Position', [580, 520, 100, 30],... 'HorizontalAlignment', 'left', 'FontSize', 10); % 滤波设置 uicontrol('Parent', app.controlPanel, 'Style', 'text', 'String', '高通滤波(Hz):',... 'Position', [20, 470, 100, 25], 'FontSize', 10); app.highPassEdit = uicontrol('Parent', app.controlPanel, 'Style', 'edit',... 'String', '0.5', 'Position', [130, 470, 60, 25], 'FontSize', 10); uicontrol('Parent', app.controlPanel, 'Style', 'text', 'String', '低通滤波(Hz):',... 'Position', [20, 440, 100, 25], 'FontSize', 10); app.lowPassEdit = uicontrol('Parent', app.controlPanel, 'Style', 'edit',... 'String', '100', 'Position', [130, 440, 60, 25], 'FontSize', 10); uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton', 'String', '应用滤波',... 'Position', [200, 450, 80, 30], 'Callback', @app.applyFilters); % 工频陷波 app.notchCheckbox = uicontrol('Parent', app.controlPanel, 'Style', 'checkbox',... 'String', '50Hz陷波', 'Position', [300, 450, 100, 25], 'Value', 1, 'FontSize', 10); % 数据记录 app.recordButton = uicontrol('Parent', app.controlPanel, 'Style', 'togglebutton',... 'String', '开始记录', 'Position', [20, 380, 100, 30],... 'Callback', @app.toggleRecording); app.saveButton = uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '保存数据', 'Position', [140, 380, 100, 30],... 'Callback', @app.saveData); % 导联选择 uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '导联选择:', 'Position', [20, 340, 80, 25], 'FontSize', 10); app.leadSelect = uicontrol('Parent', app.controlPanel, 'Style', 'popup',... 'String', app.leadNames, 'Position', [110, 340, 100, 25],... 'Callback', @app.onLeadSelected, 'FontSize', 10); % 打开导联窗口按钮 uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '打开导联窗口', 'Position', [230, 340, 120, 30],... 'Callback', @app.openSelectedLeadWindow); % 心率显示 uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '心率:', 'Position', [20, 300, 50, 25], 'FontSize', 10); app.heartRateText = uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '-- bpm', 'Position', [80, 300, 80, 25], 'FontSize', 10); % 数据导航 uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '数据位置:', 'Position', [20, 260, 80, 25], 'FontSize', 10); app.dataSlider = uicontrol('Parent', app.controlPanel, 'Style', 'slider',... 'Position', [110, 260, 300, 25], 'Callback', @app.sliderMoved,... 'Enable', 'off'); app.positionText = uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '0/0', 'Position', [420, 260, 80, 25], 'FontSize', 10); % 视图控制按钮 uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '显示总览窗口', 'Position', [20, 200, 120, 30],... 'Callback', @app.showOverviewWindow); uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '显示所有导联窗口', 'Position', [160, 200, 150, 30],... 'Callback', @app.showAllLeadWindows); uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '关闭所有导联窗口', 'Position', [330, 200, 150, 30],... 'Callback', @app.closeAllLeadWindows); % 频谱分析 spectrumPanel = uipanel('Parent', app.controlPanel, 'Title', '频谱分析',... 'Position', [0.05, 0.05, 0.9, 0.15]); app.spectrumAx = axes('Parent', spectrumPanel, 'Position', [0.1, 0.2, 0.85, 0.7]); title(app.spectrumAx, 'ECG信号频谱'); xlabel(app.spectrumAx, '频率(Hz)'); ylabel(app.spectrumAx, '幅度(dB)'); grid(app.spectrumAx, 'on'); end function initializeSystem(app) % 初始化系统参数 app.sampleRate = 1000; app.dataLength = 0; app.currentIndex = 1; app.isRecording = false; % 初始化串口参数(默认值需符合OEM协议) app.comPort = 'COM1'; app.baudRate = 115200; app.dataBits = 8; app.parity = 'none'; app.stopBits = 1; % 设置默认患者信息 app.patientInfo = struct(... 'name', '张三', ... 'gender', '男', ... 'age', 45, ... 'id', 'P2023001', ... 'date', datestr(now, 'yyyy-mm-dd')); end %% 窗口管理功能 function showOverviewWindow(app, ~, ~) % 显示总览窗口 if ishandle(app.overviewFig) figure(app.overviewFig); else app.createOverviewWindow(); end end function showAllLeadWindows(app, ~, ~) % 显示所有导联窗口 for i = 1:12 app.createSingleLeadWindow(i); end end function closeAllLeadWindows(app, ~, ~) % 关闭所有导联窗口 for i = 1:12 if ishandle(app.singleLeadFigs(i)) delete(app.singleLeadFigs(i)); end end end function openSelectedLeadWindow(app, ~, ~) % 打开选中的导联窗口 leadIdx = get(app.leadSelect, 'Value'); app.createSingleLeadWindow(leadIdx); end function onLeadSelected(app, src, ~) % 导联选择回调 leadIdx = get(src, 'Value'); app.updateSpectrumForLead(leadIdx); end function onOverviewClose(app, ~, ~) % 总览窗口关闭回调 delete(app.overviewFig); app.overviewFig = gobjects(1); end function onSingleLeadClose(app, leadIndex) % 单导联窗口关闭回调 delete(app.singleLeadFigs(leadIndex)); app.singleLeadFigs(leadIndex) = gobjects(1); end %% 通信协议实现 function setCommunication(app, ~, ~) % 串口设置对话框 prompt = {'串口号:', '波特率:', '数据位:', '校验位:', '停止位:'}; dlgtitle = '串口设置'; dims = [1 20]; definput = {app.comPort, num2str(app.baudRate), num2str(app.dataBits), ... app.parity, num2str(app.stopBits)}; answer = inputdlg(prompt, dlgtitle, dims, definput); if ~isempty(answer) app.comPort = answer{1}; app.baudRate = str2double(answer{2}); app.dataBits = str2double(answer{3}); app.parity = answer{4}; app.stopBits = str2double(answer{5}); % 验证波特率是否合法(示例:仅允许常见波特率) validBaudRates = [9600, 19200, 38400, 57600, 115200, 230400]; if ~any(app.baudRate == validBaudRates) app.baudRate = 115200; % 默认恢复为常用值 warndlg('波特率设置不合法,已恢复为115200', '设置警告'); end set(app.statusText, 'String', '串口设置已更新'); end end function connectDevice(app, ~, ~) try % 关闭现有连接 if ~isempty(app.serialPort) && isvalid(app.serialPort) fclose(app.serialPort); delete(app.serialPort); end % 创建串口对象(根据OEM协议配置参数) app.serialPort = serialport(app.comPort, app.baudRate, ... 'DataBits', app.dataBits, ... 'Parity', app.parity, ... 'StopBits', app.stopBits); % 设置基于字节的回调机制(符合OEM协议) configureCallback(app.serialPort, "byte", 1, ... @(src, ~) app.frameProcessor(src)); % 初始化数据缓冲区 app.ecgData = zeros(12, 10000); app.filteredData = zeros(12, 10000); app.timeStamps = zeros(1, 10000); app.currentIndex = 1; app.dataLength = 0; % 启用数据导航 set(app.dataSlider, 'Enable', 'on'); set(app.statusText, 'String', ['已连接到串口: ' app.comPort ' (波特率: ' num2str(app.baudRate) ')']); app.isConnected = true; catch ME errordlg(['串口连接失败: ' ME.message], '连接错误'); app.isConnected = false; end end function disconnect(app, ~, ~) if ~isempty(app.serialPort) && isvalid(app.serialPort) fclose(app.serialPort); delete(app.serialPort); app.serialPort = []; set(app.statusText, 'String', '已断开串口连接'); app.isRecording = false; app.isConnected = false; end end function frameProcessor(app, src) if src.NumBytesAvailable > 0 % OEM协议帧处理器实现 byte = read(src, 1, 'uint8'); % 帧起始检测 (0xAA) if byte == 0xAA && ~app.inFrame app.inFrame = true; app.frameBuffer = uint8(byte); return; end % 帧接收中 if app.inFrame app.frameBuffer(end+1) = byte; % 帧长度检测 (第2字节为DLC) if length(app.frameBuffer) == 2 app.frameLength = byte + 3; % DLC + CRC + EOF end % 帧结束检测 (0x55) if length(app.frameBuffer) >= app.frameLength if byte == 0x55 % 处理有效数据帧(去除帧头、CRC和帧尾) dataFrame = app.frameBuffer(3:end-2); if app.checkCRC(dataFrame) app.processECGFrame(dataFrame); else warning('接收到CRC校验失败的帧'); end end app.inFrame = false; app.frameBuffer = []; end end end end function processECGFrame(app, dataFrame) % 解析OEM协议ECG数据帧 % 假设帧格式: [ID][DLC][12导联数据(2字节/导联)][CRC] if length(dataFrame) >= 25 % 12导联*2字节 + 其他信息 % 提取12导联数据(假设每导联2字节) ecgValues = zeros(12, 1); for i = 1:12 byte1 = dataFrame(2+i*2-2); byte2 = dataFrame(2+i*2-1); ecgValues(i) = typecast(uint16(bitshift(uint8(byte2),8) + uint8(byte1)), 'int16'); ecgValues(i) = ecgValues(i) * 0.01; % 转换为mV单位 end % 提取时间戳(假设前4字节为时间戳) timestamp = typecast(uint32(bitshift(uint8(dataFrame(3)),24) + ... bitshift(uint8(dataFrame(4)),16) + ... bitshift(uint8(dataFrame(5)),8) + ... uint8(dataFrame(6))), 'uint32'); % 存储数据 if app.dataLength < size(app.ecgData, 2) app.dataLength = app.dataLength + 1; end app.timeStamps(app.dataLength) = timestamp / 1000; % 转换为秒 app.ecgData(:, app.dataLength) = ecgValues; % 实时滤波 app.realTimeFiltering(app.dataLength); % 更新显示 app.updatePlots(); % 实时分析 if mod(app.dataLength, 50) == 0 app.realTimeAnalysis(); end % 更新数据导航 set(app.dataSlider, 'Value', app.dataLength / size(app.ecgData, 2)); set(app.positionText, 'String',... sprintf('%d/%d', app.dataLength, size(app.ecgData, 2))); end end function valid = checkCRC(app, data) % CRC-16校验实现 crc = uint16(0xFFFF); for i = 1:length(data) crc = bitxor(crc, bitshift(uint16(data(i)), 8)); for j = 1:8 if bitand(crc, 0x8000) crc = bitxor(bitshift(crc, 1), 0x1021); else crc = bitshift(crc, 1); end end end valid = (crc == 0); end function startSession(app, sessionType) % UDS会话管理服务实现(0x10服务) if sessionType == 1 && app.securityLevel >= 2 app.currentSession = sessionType; % 发送肯定响应(示例) response = [0x50, sessionType]; app.sendUDSResponse(response); set(findobj(app.controlPanel, 'String', '会话:'), 'String', '会话: 扩展诊断'); else % 发送否定响应(安全访问拒绝) response = [0x7F, 0x10, 0x33]; app.sendUDSResponse(response); warndlg('安全访问级别不足,无法启动扩展诊断会话', '安全错误'); end end function sendUDSResponse(app, data) % 发送UDS协议响应帧 if isvalid(app.serialPort) % 构建完整帧: 帧头(0xAA) + DLC + 数据 + CRC + 帧尾(0x55) dlc = uint8(length(data)); frame = [uint8(0xAA), dlc, data]; % 计算CRC crc = app.calculateCRC(frame); frame = [frame, uint8(bitand(crc, 0xFF)), uint8(bitshift(crc, -8)), uint8(0x55)]; % 发送帧 write(app.serialPort, frame, 'uint8'); end end function crc = calculateCRC(app, data) % 计算CRC-16校验值 crc = uint16(0xFFFF); for i = 1:length(data) crc = bitxor(crc, bitshift(uint16(data(i)), 8)); for j = 1:8 if bitand(crc, 0x8000) crc = bitxor(bitshift(crc, 1), 0x1021); else crc = bitshift(crc, 1); end end end end %% 数据处理模块 function realTimeFiltering(app, idx) % 实时滤波处理 if idx == 1 % 初始化滤波数据 app.filteredData(:, idx) = app.ecgData(:, idx); return; end % 获取滤波参数 hpFreq = str2double(get(app.highPassEdit, 'String')); lpFreq = str2double(get(app.lowPassEdit, 'String')); useNotch = get(app.notchCheckbox, 'Value'); for i = 1:12 % 应用高通滤波 if ~isnan(hpFreq) && hpFreq > 0 [b, a] = butter(2, hpFreq/(app.sampleRate/2), 'high'); if idx > 10 segment = app.ecgData(i, max(1, idx-10):idx); filteredSeg = filtfilt(b, a, segment); app.filteredData(i, idx) = filteredSeg(end); else app.filteredData(i, idx) = app.ecgData(i, idx); end else app.filteredData(i, idx) = app.ecgData(i, idx); end % 应用低通滤波 if ~isnan(lpFreq) && lpFreq > 0 [b, a] = butter(4, lpFreq/(app.sampleRate/2), 'low'); if idx > 10 segment = app.filteredData(i, max(1, idx-10):idx); filteredSeg = filtfilt(b, a, segment); app.filteredData(i, idx) = filteredSeg(end); end end % 应用50Hz陷波 if useNotch wo = 50/(app.sampleRate/2); bw = wo/10; [b, a] = iirnotch(wo, bw); if idx > 10 segment = app.filteredData(i, max(1, idx-10):idx); filteredSeg = filtfilt(b, a, segment); app.filteredData(i, idx) = filteredSeg(end); end end end end function applyFilters(app, ~, ~) % 应用全局滤波 if app.dataLength > 0 % 获取滤波参数 hpFreq = str2double(get(app.highPassEdit, 'String')); lpFreq = str2double(get(app.lowPassEdit, 'String')); useNotch = get(app.notchCheckbox, 'Value'); for i = 1:12 % 高通滤波 if ~isnan(hpFreq) && hpFreq > 0 [b, a] = butter(2, hpFreq/(app.sampleRate/2), 'high'); app.filteredData(i, 1:app.dataLength) = ... filtfilt(b, a, app.ecgData(i, 1:app.dataLength)); end % 低通滤波 if ~isnan(lpFreq) && lpFreq > 0 [b, a] = butter(4, lpFreq/(app.sampleRate/2), 'low'); app.filteredData(i, 1:app.dataLength) = ... filtfilt(b, a, app.filteredData(i, 1:app.dataLength)); end % 50Hz陷波 if useNotch wo = 50/(app.sampleRate/2); bw = wo/10; [b, a] = iirnotch(wo, bw); app.filteredData(i, 1:app.dataLength) = ... filtfilt(b, a, app.filteredData(i, 1:app.dataLength)); end end % 更新显示 app.updatePlots(); app.updateSpectrum(); set(app.statusText, 'String', '全局滤波已应用'); end end %% 实时分析模块 function realTimeAnalysis(app) % 实时分析ECG信号 if app.dataLength > 100 % 在II导联检测R波 leadIndex = 2; data = app.filteredData(leadIndex, max(1, app.dataLength-500):app.dataLength); % 使用Pan-Tompkins算法检测R波 [~, qrs_i_raw] = app.pan_tompkins(data, app.sampleRate); % 计算心率 if length(qrs_i_raw) > 1 rrIntervals = diff(qrs_i_raw) / app.sampleRate; app.heartRate = 60 / mean(rrIntervals); set(app.heartRateText, 'String', sprintf('%.1f bpm', app.heartRate)); % 更新图形标记 if ishandle(app.leadPlots(leadIndex)) ax = get(app.leadPlots(leadIndex), 'Parent'); hold(ax, 'on'); if ~isempty(findobj(ax, 'Tag', 'RPeakMarker')) delete(findobj(ax, 'Tag', 'RPeakMarker')); end % 计算实际时间位置 timeOffset = app.timeStamps(max(1, app.dataLength-500)); timePos = app.timeStamps(qrs_i_raw + max(1, app.dataLength-500) - 1); plot(ax, timePos, data(qrs_i_raw), 'ro', 'MarkerSize', 6, 'MarkerFaceColor', 'r', 'Tag', 'RPeakMarker'); hold(ax, 'off'); end end end end function [qrs_amp_raw, qrs_i_raw] = pan_tompkins(~, ecg, fs) % Pan-Tompkins R波检测算法 % 1. 带通滤波 (5-15 Hz) f1 = 5/(fs/2); f2 = 15/(fs/2); [b, a] = butter(1, [f1, f2], 'bandpass'); ecg_filtered = filtfilt(b, a, ecg); % 2. 微分 diff_ecg = diff(ecg_filtered); diff_ecg = [diff_ecg(1), diff_ecg]; % 3. 平方 sqr_ecg = diff_ecg .^ 2; % 4. 移动平均窗口积分 window_size = round(0.150 * fs); integrated_ecg = movmean(sqr_ecg, window_size); % 5. 自适应阈值检测 max_h = max(integrated_ecg); threshold = 0.2 * max_h; % 6. 寻找R波位置 [qrs_amp_raw, qrs_i_raw] = findpeaks(integrated_ecg,... 'MinPeakHeight', threshold,... 'MinPeakDistance', round(0.2*fs)); end %% 数据管理 function toggleRecording(app, src, ~) % 开始/停止记录数据 if src.Value app.isRecording = true; set(src, 'String', '停止记录'); set(app.statusText, 'String', '记录中...'); % 初始化记录缓冲区 app.ecgData = zeros(12, 10000); app.filteredData = zeros(12, 10000); app.timeStamps = zeros(1, 10000); app.currentIndex = 1; app.dataLength = 0; else app.isRecording = false; set(src, 'String', '开始记录'); set(app.statusText, 'String', '记录已停止'); end end function loadDataFile(app, ~, ~) % 加载ECG数据文件 [filename, pathname] = uigetfile({'*.mat;*.csv;*.txt', '数据文件 (*.mat, *.csv, *.txt)'},... '选择ECG数据文件'); if isequal(filename, 0) return; end filepath = fullfile(pathname, filename); [~, ~, ext] = fileparts(filepath); try h = waitbar(0, '加载数据中...', 'Name', '数据处理'); switch lower(ext) case '.mat' data = load(filepath); waitbar(0.3, h); if isfield(data, 'ecgData') app.ecgData = data.ecgData; app.filteredData = data.filteredData; app.timeStamps = data.timeStamps; app.sampleRate = data.sampleRate; else error('无法识别MAT文件中的数据格式'); end case {'.csv', '.txt'} data = readmatrix(filepath); waitbar(0.3, h); if size(data, 2) >= 13 app.timeStamps = data(:, 1)'; app.ecgData = data(:, 2:13)'; app.sampleRate = 1 / mean(diff(app.timeStamps)); else error('CSV/TXT文件应包含时间戳和12导联数据'); end end app.dataLength = size(app.ecgData, 2); app.currentIndex = app.dataLength; % 应用滤波 waitbar(0.7, h, '应用滤波...'); app.applyFilters(); % 更新界面 waitbar(0.9, h, '更新界面...'); app.updatePlots(); app.updateSpectrum(); % 配置导航滑块 set(app.dataSlider, 'Min', 0, 'Max', 1, 'Value', 1,... 'SliderStep', [100/app.dataLength, 1000/app.dataLength],... 'Enable', 'on'); set(app.positionText, 'String',... sprintf('%d/%d', app.dataLength, size(app.ecgData, 2))); set(app.statusText, 'String',... sprintf('已加载: %s (%d个样本)', filename, app.dataLength)); waitbar(1, h, '完成!'); pause(0.5); delete(h); catch ME errordlg(sprintf('加载失败: %s', ME.message), '文件错误'); end end function saveData(app, ~, ~) % 保存ECG数据 if app.dataLength == 0 errordlg('没有可保存的数据', '保存错误'); return; end [filename, pathname] = uiputfile({'*.mat', 'MAT文件 (*.mat)'; '*.csv', 'CSV文件 (*.csv)'},... '保存ECG数据', 'ecg_data.mat'); if isequal(filename, 0) return; end filepath = fullfile(pathname, filename); [~, ~, ext] = fileparts(filepath); try app.ecgData = app.ecgData(:, 1:app.dataLength); app.filteredData = app.filteredData(:, 1:app.dataLength); app.timeStamps = app.timeStamps(1:app.dataLength); app.sampleRate = app.sampleRate; switch lower(ext) case '.mat' save(filepath, 'ecgData', 'filteredData', 'timeStamps', 'sampleRate'); case '.csv' dataToSave = [app.timeStamps', app.ecgData']; writematrix(dataToSave, filepath); end set(app.statusText, 'String', sprintf('数据已保存至: %s', filename)); catch ME errordlg(sprintf('保存失败: %s', ME.message), '保存错误'); end end %% 显示更新 function updatePlots(app) % 更新所有窗口的心电图显示 if app.dataLength > 0 windowSize = 500; % 显示窗口大小 startIdx = max(1, app.currentIndex - windowSize); endIdx = min(app.dataLength, app.currentIndex + windowSize); timeWindow = app.timeStamps(startIdx:endIdx); % 更新总览窗口 if ishandle(app.overviewFig) for i = 1:12 if ishandle(app.overviewPlots(i)) set(app.overviewPlots(i), 'XData', timeWindow,... 'YData', app.filteredData(i, startIdx:endIdx)); % 自动调整Y轴范围 dataRange = range(app.filteredData(i, startIdx:endIdx)); if dataRange > 0 ax = get(app.overviewPlots(i), 'Parent'); ylim(ax, [min(app.filteredData(i, startIdx:endIdx)) - 0.1*dataRange,... max(app.filteredData(i, startIdx:endIdx)) + 0.1*dataRange]); end end end drawnow; end % 更新单导联窗口 for i = 1:12 if ishandle(app.singleLeadFigs(i)) && ishandle(app.leadPlots(i)) set(app.leadPlots(i), 'XData', timeWindow,... 'YData', app.filteredData(i, startIdx:endIdx)); % 自动调整Y轴范围 dataRange = range(app.filteredData(i, startIdx:endIdx)); if dataRange > 0 ax = get(app.leadPlots(i), 'Parent'); ylim(ax, [min(app.filteredData(i, startIdx:endIdx)) - 0.1*dataRange,... max(app.filteredData(i, startIdx:endIdx)) + 0.1*dataRange]); end drawnow; end end end end function updateSpectrumForLead(app, leadIndex) % 更新指定导联的频谱分析 if app.dataLength > 0 && leadIndex >= 1 && leadIndex <= 12 signal = app.filteredData(leadIndex, max(1, app.dataLength-2000):app.dataLength); % 计算FFT N = length(signal); if N < 10, return; end f = (0:N-1)*(app.sampleRate/N); Y = fft(signal); P2 = abs(Y/N); P1 = P2(1:floor(N/2)+1); P1(2:end-1) = 2*P1(2:end-1); f = f(1:floor(N/2)+1); % 更新频谱分析图 plot(app.spectrumAx, f, 20*log10(P1), 'b'); title(app.spectrumAx, sprintf('导联 %s 频谱', app.leadNames{leadIndex})); xlabel(app.spectrumAx, '频率 (Hz)'); ylabel(app.spectrumAx, '幅度 (dB)'); xlim(app.spectrumAx, [0, min(150, app.sampleRate/2)]); grid(app.spectrumAx, 'on'); end end function showSpectrumForLead(app, leadIndex) % 在单独窗口中显示导联频谱 if app.dataLength > 0 && leadIndex >= 1 && leadIndex <= 12 figName = sprintf('导联 %s 频谱分析', app.leadNames{leadIndex}); app.fig = figure('Name', figName, 'NumberTitle', 'off',... 'Position', [200, 200, 600, 400]); ax = axes('Parent', app.fig, 'Position', [0.1, 0.15, 0.85, 0.75]); signal = app.filteredData(leadIndex, max(1, app.dataLength-2000):app.dataLength); N = length(signal); f = (0:N-1)*(app.sampleRate/N); Y = fft(signal); P2 = abs(Y/N); P1 = P2(1:floor(N/2)+1); P1(2:end-1) = 2*P1(2:end-1); f = f(1:floor(N/2)+1); plot(ax, f, 20*log10(P1), 'b'); title(ax, sprintf('导联 %s 频谱分析', app.leadNames{leadIndex})); xlabel(ax, '频率 (Hz)'); ylabel(ax, '幅度 (dB)'); xlim(ax, [0, min(150, app.sampleRate/2)]); grid(ax, 'on'); end end function detectRPeaksForLead(app, leadIndex) % 检测指定导联的R波 if app.dataLength > 0 && leadIndex >= 1 && leadIndex <= 12 data = app.filteredData(leadIndex, max(1, app.dataLength-2000):app.dataLength); % 使用Pan-Tompkins算法检测R波 [~, qrs_i_raw] = app.pan_tompkins(data, app.sampleRate); % 在图形上标记R波 if ishandle(app.singleLeadFigs(leadIndex)) app.fig = app.singleLeadFigs(leadIndex); ax = findobj(app.fig, 'Type', 'axes'); % 清除旧标记 oldMarkers = findobj(ax, 'Tag', 'RPeakMarker'); if ~isempty(oldMarkers) delete(oldMarkers); end % 添加新标记 hold(ax, 'on'); plot(ax, app.timeStamps(qrs_i_raw + max(1, app.dataLength-2000) - 1),... data(qrs_i_raw), 'ro', 'MarkerSize', 8, 'MarkerFaceColor', 'r',... 'Tag', 'RPeakMarker'); hold(ax, 'off'); % 计算并显示心率 if length(qrs_i_raw) > 1 rrIntervals = diff(qrs_i_raw) / app.sampleRate; app.heartRate = 60 / mean(rrIntervals); title(ax, sprintf('导联 %s - 心率: %.1f bpm',... app.leadNames{leadIndex}, app.heartRate)); end end end end %% 报告生成 function setPatientInfo(app, ~, ~) % 设置患者信息 prompt = {'姓名:', '性别:', '年龄:', '患者ID:'}; dlgtitle = '患者信息'; dims = [1 25]; definput = {app.patientInfo.name, app.patientInfo.gender,... num2str(app.patientInfo.age), app.patientInfo.id}; answer = inputdlg(prompt, dlgtitle, dims, definput); if ~isempty(answer) app.patientInfo.name = answer{1}; app.patientInfo.gender = answer{2}; app.patientInfo.age = str2double(answer{3}); app.patientInfo.id = answer{4}; app.patientInfo.date = datestr(now, 'yyyy-mm-dd HH:MM'); end end function generateReport(app, ~, ~) % 生成心电图报告 if app.dataLength == 0 errordlg('没有数据可生成报告', '报告错误'); return; end [filename, pathname] = uiputfile('*.pdf', '保存心电图报告', 'ECG_Report.pdf'); if filename == 0 return; end pdfFile = fullfile(pathname, filename); app.exportToPDF(pdfFile); msgbox(sprintf('心电图报告已保存至:\n%s', pdfFile), '报告生成'); end function exportToPDF(app, filename) % 导出PDF报告 app.fig = figure('Visible', 'off', 'Position', [100, 100, 1200, 900]); % 报告标题 uicontrol('Style', 'text', 'String', '心电图报告',... 'Position', [400, 850, 400, 40], 'FontSize', 20, 'FontWeight', 'bold'); % 患者信息 app.patientInfo = {sprintf('姓名: %s', app.patientInfo.name),... sprintf('性别: %s', app.patientInfo.gender),... sprintf('年龄: %d', app.patientInfo.age),... sprintf('ID: %s', app.patientInfo.id),... sprintf('检查日期: %s', app.patientInfo.date)}; uicontrol('Style', 'text', 'String', app.patientInfo,... 'Position', [50, 780, 300, 100], 'FontSize', 12, 'HorizontalAlignment', 'left'); % 分析结果 analysisInfo = {sprintf('心率: %.1f bpm', app.heartRate),... sprintf('采样率: %d Hz', app.sampleRate),... sprintf('数据长度: %.1f 秒', app.timeStamps(app.dataLength))}; uicontrol('Style', 'text', 'String', analysisInfo,... 'Position', [50, 680, 300, 60], 'FontSize', 12, 'HorizontalAlignment', 'left'); % 绘制12导联心电图 for i = 1:12 row = ceil(i/3); col = mod(i-1,3)+1; ax = subplot(4,3,i, 'Parent', app.fig); if app.dataLength > 0 % 显示完整数据 plot(ax, app.timeStamps(1:app.dataLength),... app.filteredData(i, 1:app.dataLength), 'b-'); end title(ax, app.leadNames{i}); xlabel(ax, '时间(s)'); ylabel(ax, '幅度(mV)'); grid(ax, 'on'); end % 导出为PDF exportgraphics(app.fig, filename, 'ContentType', 'vector'); close(app.fig); end %% 系统控制 function onClose(app, ~, ~) % 关闭应用程序 app.disconnect(); % 关闭所有窗口 if ishandle(app.overviewFig) delete(app.overviewFig); end for i = 1:12 if ishandle(app.singleLeadFigs(i)) delete(app.singleLeadFigs(i)); end end delete(app.fig); end end end % 辅助函数 function result = iff(condition, trueValue, falseValue) if condition result = trueValue; else result = falseValue; end end
06-16
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值