当前位置:首页>APP源码>嵌入式软件单元测试实战:如何让你的MCU代码在PC上跑起来?

嵌入式软件单元测试实战:如何让你的MCU代码在PC上跑起来?

  • 2026-06-26 05:13:15
嵌入式软件单元测试实战:如何让你的MCU代码在PC上跑起来?

做嵌入式开发的工程师,对"单元测试"这个词大概都不陌生。但要说真正在项目里落地了单元测试的,坦白讲,比例不高。

原因也很直接:嵌入式代码跟硬件绑得太紧了。你想测一个温度采集函数,结果它里面直接调了ADC寄存器;你想测一个通信协议的解析逻辑,结果它跟串口驱动缠在一起。代码离开了目标板根本跑不起来,而在板子上做自动化测试,成本又高得离谱。

久而久之,很多嵌入式团队形成了一种默认共识:"我们的代码没法做单元测试。"

但事实是,嵌入式软件不仅可以做单元测试,而且如果方法对了,测试效率比你想象的高得多。 关键在于你得理解一个核心前提:单元测试测的不是硬件,而是逻辑。只要你能把逻辑从硬件依赖中剥离出来,它就能在PC上跑,就能自动化,就能纳入CI流程。

这篇文章,我会从嵌入式单元测试的实际挑战讲起,介绍如何搭建基于Host的测试环境,如何用Mock/Stub替代硬件依赖,如何选择测试框架,以及如何在工程中真正把这件事落地。不讲理论大道理,只聊工程中能用上的实操方案。


一、嵌入式单元测试为什么难?

在聊怎么做之前,先把问题摆清楚。嵌入式单元测试落地难,不是因为工程师不重视测试,而是嵌入式开发有几个天然的障碍,导致常规的单元测试方法直接搬过来水土不服。

1.1 硬件依赖无处不在

这是最核心的障碍。嵌入式代码大量直接操作寄存器、调用HAL库函数、访问外设。一个看起来很简单的业务函数,背后可能拖着一串硬件调用链:

float get_temperature(void){    uint16_t adc_raw = HAL_ADC_GetValue(&hadc1);    // 依赖ADC硬件    float voltage = adc_raw * 3.3f / 4096.0f;    float temp = (voltage - 0.5f) * 100.0f;    return temp;}

这个函数在PC上编译都过不了,因为HAL_ADC_GetValuehadc1根本不存在。

1.2 交叉编译环境的隔阂

嵌入式项目用的是交叉编译工具链(比如arm-none-eabi-gcc),目标平台是Cortex-M、RISC-V等处理器。而单元测试通常需要在开发主机上运行,用的是主机端编译器(gcc/msvc)。这两套编译环境之间存在天然的隔阂,头文件、链接库、数据类型宽度都可能不一样。

1.3 "在板上测"的高成本

有人说,那我直接在板子上跑测试不就行了?可以,但成本很高:

  • • 每次跑测试都要编译、烧录、复位,周期长
  • • 需要物理设备,不能并行、不好自动化
  • • 测试结果通常通过串口输出,采集和分析不方便
  • • 多人协作时,开发板成了稀缺资源

1.4 缺乏测试意识和基础设施

很多嵌入式项目从立项开始就没有考虑过可测试性。代码里业务逻辑和硬件操作混在一起,模块之间通过全局变量通信,函数副作用多、接口不清晰。等到想补测试的时候,发现代码结构根本不支持。

这几个问题叠加在一起,就形成了嵌入式测试的困境:

那思路是什么?接下来就讲。


二、核心思路:把"硬件"和"逻辑"拆开

嵌入式单元测试能不能落地,取决于一个关键动作:把业务逻辑从硬件依赖中剥离出来。

这不是什么新概念。做过桌面端或服务器端开发的人都知道,单元测试的基本原则就是隔离被测单元的外部依赖。在嵌入式领域,最主要的"外部依赖"就是硬件。

2.1 分层是前提

要实现逻辑和硬件的分离,代码必须有基本的分层结构。至少要分出三层:

中间那层"硬件抽象层"是关键。它像一道隔离墙,让上层的业务逻辑通过一组抽象接口来访问硬件能力,而不是直接去碰寄存器。

2.2 举个具体的例子

还是前面那个温度采集的函数。原来的写法是直接调用HAL库:

// 原始写法:业务逻辑和硬件耦合在一起float get_temperature(void){    uint16_t adc_raw = HAL_ADC_GetValue(&hadc1);    float voltage = adc_raw * 3.3f / 4096.0f;    float temp = (voltage - 0.5f) * 100.0f;    return temp;}

重构之后,我们把ADC读取抽象出来:

// bsp_adc.h - 硬件抽象接口uint16_t bsp_adc_read_channel(uint8_t channel);
// temperature.c - 业务逻辑,只依赖抽象接口#include "bsp_adc.h"#define ADC_CHANNEL_TEMP  0float get_temperature(void){    uint16_t adc_raw = bsp_adc_read_channel(ADC_CHANNEL_TEMP);    float voltage = adc_raw * 3.3f / 4096.0f;    float temp = (voltage - 0.5f) * 100.0f;    return temp;}

现在get_temperature()只依赖一个接口函数bsp_adc_read_channel(),而不是直接绑定在STM32的HAL库上。在PC端做测试的时候,我们只需要提供一个假的bsp_adc_read_channel()实现,就可以让这段代码跑起来了。

这就是Mock的用武之地,后面会详细展开。

2.3 对已有项目怎么办?

如果你的项目已经写了一大堆跟硬件耦合的代码,全部重构不太现实。务实的做法是:

  1. 1. 新模块从一开始就按分层思路写,确保可测试
  2. 2. 对核心模块逐步抽离,优先处理出过bug、逻辑复杂的模块
  3. 3. 不追求100%覆盖率,先把最容易出问题的逻辑覆盖住

完美主义是单元测试落地的敌人。能测60%的核心逻辑,比什么都不测强太多了。


三、搭建基于Host的测试环境

所谓"Host-based Testing",就是把嵌入式代码中的纯逻辑部分拿到PC上,用主机端的编译器编译运行。这是嵌入式单元测试最主流也最实用的方案。

3.1 基本原理

整体思路如下图所示:

左边是正常的嵌入式工程,右边是测试工程。测试工程复用业务逻辑层的源码(不是复制,是直接引用同一份.c文件),但把底层的硬件抽象层替换成Mock实现。

3.2 工程目录组织

一个支持Host-based测试的嵌入式工程,目录结构通常是这样的:

project/├── src/                      # 产品源码│   ├── app/                  # 业务逻辑层(单元测试目标)│   │   ├── temperature.c│   │   ├── alarm.c│   │   └── protocol_parser.c│   ├── bsp/                  # 硬件抽象层│   │   ├── bsp_adc.h         # 抽象接口定义│   │   ├── bsp_adc_stm32.c   # STM32平台实现│   │   ├── bsp_gpio.h│   │   └── bsp_gpio_stm32.c│   └── driver/               # 底层驱动│       └── ...├── test/                     # 测试工程(PC端)│   ├── mocks/                # Mock实现│   │   ├── mock_bsp_adc.c│   │   └── mock_bsp_gpio.c│   ├── test_temperature.c    # 测试用例│   ├── test_alarm.c│   ├── test_protocol_parser.c│   └── CMakeLists.txt        # 测试工程的构建脚本└── CMakeLists.txt            # 主工程的构建脚本

核心要点:src/app/下面的代码只依赖bsp/*.h中定义的接口,不依赖任何具体平台的实现。测试工程编译时,链接的是test/mocks/下的Mock实现,而不是bsp/*_stm32.c

3.3 用CMake管理双工程

用CMake可以很方便地同时管理产品工程和测试工程。测试工程的CMakeLists.txt大致长这样:

cmake_minimum_required(VERSION 3.14)project(unit_tests C)# 测试框架(以Unity为例)add_subdirectory(unity)# 被测源码set(SRC_DIR ${CMAKE_SOURCE_DIR}/../src)# 测试可执行文件add_executable(test_temperature    test_temperature.c    ${SRC_DIR}/app/temperature.c   # 被测模块    mocks/mock_bsp_adc.c           # Mock替身)target_include_directories(test_temperature PRIVATE    ${SRC_DIR}/app    ${SRC_DIR}/bsp      # 引用bsp头文件中的接口定义    unity/src)target_link_libraries(test_temperature unity)enable_testing()add_test(NAME test_temperature COMMAND test_temperature)

这样一来,make test就能在PC上编译并运行测试了。


四、Mock和Stub:替换硬件依赖的两把利刃

有了分层结构和Host测试环境,接下来最关键的技术环节就是:如何在测试中替换掉那些硬件相关的函数?

这就要用到Mock和Stub。这两个概念经常被混用,但在实践中有明确的区分。

4.1 Stub:最简单的替身

Stub是最轻量的替代方式。它只是提供一个假实现,让代码能编译通过、跑起来,返回一个预设的值。

// mock_bsp_adc.c - Stub实现static uint16_t fake_adc_value = 0;// 提供给测试用例设置返回值的接口void mock_bsp_adc_set_value(uint16_t value){    fake_adc_value = value;}// Stub: 替代真实的ADC读取uint16_t bsp_adc_read_channel(uint8_t channel){    (void)channel;    return fake_adc_value;}

测试用例这样写:

// test_temperature.c#include "unity.h"#include "temperature.h"extern void mock_bsp_adc_set_value(uint16_t value);void test_temperature_at_25_degrees(void){    // 25°C对应的ADC值:(25/100 + 0.5) / 3.3 * 4096 ≈ 930    mock_bsp_adc_set_value(930);    float temp = get_temperature();    // 允许0.5°C的误差    TEST_ASSERT_FLOAT_WITHIN(0.5f, 25.0f, temp);}void test_temperature_at_0_degrees(void){    // 0°C对应的ADC值:0.5 / 3.3 * 4096 ≈ 621    mock_bsp_adc_set_value(621);    float temp = get_temperature();    TEST_ASSERT_FLOAT_WITHIN(0.5f, 0.0f, temp);}void test_temperature_adc_returns_zero(void){    mock_bsp_adc_set_value(0);    float temp = get_temperature();    // ADC返回0时,voltage=0, temp=(0-0.5)*100=-50    TEST_ASSERT_FLOAT_WITHIN(0.1f, -50.0f, temp);}

这就是最基本的嵌入式单元测试。通过Stub控制输入(ADC返回值),验证输出(温度计算结果)。整个过程在PC上完成,不需要任何硬件。

4.2 Mock:带验证能力的替身

Stub只管"给假数据",Mock在此基础上还能验证被测代码是否按预期方式调用了依赖函数

比如,你要测试一个报警模块:当温度超过阈值时,应该调用bsp_gpio_set(PIN_ALARM, 1)来拉高报警引脚。你不仅需要提供GPIO函数的假实现,还需要验证这个函数确实被调用了,并且参数正确。

// mock_bsp_gpio.c - Mock实现static uint8_t last_pin = 0;static uint8_t last_state = 0;static int call_count = 0;void mock_bsp_gpio_reset(void){    last_pin = 0;    last_state = 0;    call_count = 0;}int mock_bsp_gpio_get_call_count(void){    return call_count;}uint8_t mock_bsp_gpio_get_last_pin(void){    return last_pin;}uint8_t mock_bsp_gpio_get_last_state(void){    return last_state;}// Mock: 替代真实的GPIO操作void bsp_gpio_set(uint8_t pin, uint8_t state){    last_pin = pin;    last_state = state;    call_count++;}

测试用例:

void test_alarm_triggers_when_over_threshold(void){    mock_bsp_gpio_reset();    // 设置温度为80°C,阈值为70°C    alarm_check(80.0f, 70.0f);    TEST_ASSERT_EQUAL(1, mock_bsp_gpio_get_call_count());    TEST_ASSERT_EQUAL(PIN_ALARM, mock_bsp_gpio_get_last_pin());    TEST_ASSERT_EQUAL(1, mock_bsp_gpio_get_last_state());}void test_alarm_not_trigger_when_below_threshold(void){    mock_bsp_gpio_reset();    alarm_check(60.0f, 70.0f);    TEST_ASSERT_EQUAL(0, mock_bsp_gpio_get_call_count());}

4.3 Stub vs Mock 的选择

什么时候用Stub,什么时候用Mock?一个简单的判断标准:

在实际项目中,大部分场景用Stub就够了。Mock主要用在你需要确认"某个动作确实被执行了"的时候。


五、测试框架选型:嵌入式场景下的务实选择

PC端的测试框架多如牛毛,但适合嵌入式项目的选择其实不多。嵌入式有自己的约束:代码是C语言为主、需要轻量级、最好能同时在Host和Target上跑。

5.1 主流框架对比

框架
语言
特点
适合场景
Unity
C
极其轻量,单文件即可运行,专为嵌入式设计
绝大多数嵌入式C项目的首选
CMock
C
配合Unity使用,自动生成Mock代码
需要大量Mock的项目
CeedlingBundle
C
Unity + CMock + 构建工具的整合方案
想要开箱即用的团队
Google Test
C++
功能丰富,生态完善
C++嵌入式项目,或混编项目
CppUTest
C/C++
为嵌入式设计,支持内存泄漏检测
关注内存安全的项目

5.2 推荐:Unity + 手写Mock

对于大多数嵌入式C项目,我推荐的组合是:Unity框架 + 手写Mock/Stub

为什么?

Unity足够轻量。 整个框架就三个文件:unity.cunity.hunity_internals.h。不需要复杂的构建配置,不依赖任何第三方库,甚至可以直接在目标板上运行(如果你需要的话)。

手写Mock可控性强。 虽然CMock能自动生成Mock代码,但自动生成的代码有时候过于复杂,调试起来不方便。对于嵌入式项目来说,硬件抽象接口通常就那么十几二十个函数,手写Mock的工作量并不大,但你对每个Mock的行为完全可控。

Unity的测试用例结构非常简单:

#include "unity.h"void setUp(void){    // 每个测试用例执行前的初始化}void tearDown(void){    // 每个测试用例执行后的清理}void test_something_works(void){    int result = function_under_test(42);    TEST_ASSERT_EQUAL(expected_value, result);}int main(void){    UNITY_BEGIN();    RUN_TEST(test_something_works);    return UNITY_END();}

常用的断言宏:

TEST_ASSERT_EQUAL(expected, actual)             // 整型比较TEST_ASSERT_EQUAL_STRING(expected, actual)       // 字符串比较TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual)// 浮点比较TEST_ASSERT_TRUE(condition)                      // 条件为真TEST_ASSERT_NULL(pointer)                        // 空指针TEST_ASSERT_EQUAL_MEMORY(expected, actual, len)  // 内存比较TEST_ASSERT_EQUAL_HEX8(expected, actual)         // 十六进制比较

这些断言覆盖了嵌入式测试中绝大部分需求。


六、完整实战:为协议解析模块编写单元测试

前面的温度采集例子比较简单,现在来看一个更贴近实际的场景:测试一个自定义协议的解析模块

协议解析是嵌入式项目里非常典型的纯逻辑模块,天然适合做单元测试,因为它处理的是字节数组到结构体的转换,不涉及任何硬件操作。

6.1 协议格式定义

假设我们的私有协议格式如下:

6.2 解析模块实现

// protocol_parser.h#ifndef PROTOCOL_PARSER_H#define PROTOCOL_PARSER_H#include <stdint.h>#include <stdbool.h>#define PROTO_HEADER_0       0xAA#define PROTO_HEADER_1       0x55#define PROTO_MAX_DATA_LEN   128typedefstruct {    uint8_t  cmd;    uint8_t  data[PROTO_MAX_DATA_LEN];    uint8_t  data_len;} proto_frame_t;typedefenum {    PROTO_OK = 0,    PROTO_ERR_HEADER,    PROTO_ERR_LENGTH,    PROTO_ERR_CRC,} proto_result_t;proto_result_t proto_parse(const uint8_t *buf, uint16_t buf_len,                           proto_frame_t *frame);uint16_t proto_calc_crc16(const uint8_t *data, uint16_t len);#endif
// protocol_parser.c#include "protocol_parser.h"uint16_t proto_calc_crc16(const uint8_t *data, uint16_t len){    uint16_t crc = 0xFFFF;    for (uint16_t i = 0; i < len; i++) {        crc ^= data[i];        for (uint8_t j = 0; j < 8; j++) {            if (crc & 0x0001)                crc = (crc >> 1) ^ 0xA001;            else                crc = crc >> 1;        }    }    return crc;}proto_result_t proto_parse(const uint8_t *buf, uint16_t buf_len,                           proto_frame_t *frame){    if (buf_len < 5)        return PROTO_ERR_LENGTH;    if (buf[0] != PROTO_HEADER_0 || buf[1] != PROTO_HEADER_1)        return PROTO_ERR_HEADER;    uint8_t payload_len = buf[2];    if (payload_len == 0 || payload_len > PROTO_MAX_DATA_LEN + 1)        return PROTO_ERR_LENGTH;    uint16_t expected_total = 2 + 1 + payload_len + 2;    if (buf_len < expected_total)        return PROTO_ERR_LENGTH;    uint16_t crc_received = (buf[expected_total - 2] << 8)                          |  buf[expected_total - 1];    uint16_t crc_calc = proto_calc_crc16(&buf[2], payload_len + 1);    if (crc_received != crc_calc)        return PROTO_ERR_CRC;    frame->cmd = buf[3];    frame->data_len = payload_len - 1;    for (uint8_t i = 0; i < frame->data_len; i++) {        frame->data[i] = buf[4 + i];    }    return PROTO_OK;}

6.3 测试用例

这是重点部分。一个好的单元测试要覆盖正常路径和各种异常路径:

// test_protocol_parser.c#include "unity.h"#include "protocol_parser.h"static proto_frame_t frame;void setUp(void) { memset(&frame, 0, sizeof(frame)); }void tearDown(void) {}// 辅助函数:构造一个完整的合法帧static uint16_t build_valid_frame(uint8_t *buf, uint8_t cmd,                                  const uint8_t *data, uint8_t data_len){    buf[0] = 0xAA;    buf[1] = 0x55;    buf[2] = data_len + 1;   // LEN = CMD + DATA    buf[3] = cmd;    for (uint8_t i = 0; i < data_len; i++)        buf[4 + i] = data[i];    uint16_t crc = proto_calc_crc16(&buf[2], data_len + 2);    uint16_t total = 4 + data_len;    buf[total]     = (crc >> 8) & 0xFF;    buf[total + 1] = crc & 0xFF;    return total + 2;}/* --- 正常场景 --- */void test_parse_valid_frame(void){    uint8_t buf[32];    uint8_t data[] = {0x01, 0x02, 0x03};    uint16_t len = build_valid_frame(buf, 0x10, data, 3);    TEST_ASSERT_EQUAL(PROTO_OK, proto_parse(buf, len, &frame));    TEST_ASSERT_EQUAL_HEX8(0x10, frame.cmd);    TEST_ASSERT_EQUAL(3, frame.data_len);    TEST_ASSERT_EQUAL_MEMORY(data, frame.data, 3);}void test_parse_frame_no_data(void){    uint8_t buf[32];    uint16_t len = build_valid_frame(buf, 0x20, NULL, 0);    TEST_ASSERT_EQUAL(PROTO_OK, proto_parse(buf, len, &frame));    TEST_ASSERT_EQUAL_HEX8(0x20, frame.cmd);    TEST_ASSERT_EQUAL(0, frame.data_len);}/* --- 异常场景 --- */void test_parse_wrong_header(void){    uint8_t buf[] = {0xBB, 0x55, 0x01, 0x10, 0x00, 0x00};    TEST_ASSERT_EQUAL(PROTO_ERR_HEADER, proto_parse(buf, 6, &frame));}void test_parse_buffer_too_short(void){    uint8_t buf[] = {0xAA, 0x55, 0x01};    TEST_ASSERT_EQUAL(PROTO_ERR_LENGTH, proto_parse(buf, 3, &frame));}void test_parse_crc_mismatch(void){    uint8_t buf[32];    uint8_t data[] = {0x01};    uint16_t len = build_valid_frame(buf, 0x10, data, 1);    buf[len - 1] ^= 0xFF;   // 故意破坏CRC    TEST_ASSERT_EQUAL(PROTO_ERR_CRC, proto_parse(buf, len, &frame));}void test_parse_length_exceeds_buffer(void){    uint8_t buf[32];    uint8_t data[] = {0x01};    uint16_t len = build_valid_frame(buf, 0x10, data, 1);    // 传入的buf_len比实际帧短    TEST_ASSERT_EQUAL(PROTO_ERR_LENGTH, proto_parse(buf, len - 1, &frame));}int main(void){    UNITY_BEGIN();    RUN_TEST(test_parse_valid_frame);    RUN_TEST(test_parse_frame_no_data);    RUN_TEST(test_parse_wrong_header);    RUN_TEST(test_parse_buffer_too_short);    RUN_TEST(test_parse_crc_mismatch);    RUN_TEST(test_parse_length_exceeds_buffer);    return UNITY_END();}

运行结果大概长这样:

test_protocol_parser.c:30:test_parse_valid_frame:PASStest_protocol_parser.c:40:test_parse_frame_no_data:PASStest_protocol_parser.c:49:test_parse_wrong_header:PASStest_protocol_parser.c:55:test_parse_buffer_too_short:PASStest_protocol_parser.c:63:test_parse_crc_mismatch:PASStest_protocol_parser.c:71:test_parse_length_exceeds_buffer:PASS-----------------------6 Tests 0 Failures 0 IgnoredOK

注意看这里的测试策略:正常帧能解析、无数据帧能解析、帧头错误能识别、缓冲区太短能拦截、CRC错误能检测、长度不匹配能发现。这6个测试用例覆盖了协议解析最核心的路径。

这整个过程在PC上几毫秒就能跑完,而且完全不需要任何硬件。


七、接入CI:让测试自动跑起来

单元测试写出来之后,如果只是偶尔在本地手动跑一下,效果会大打折扣。真正发挥价值的方式是把测试接入CI(持续集成),让每次代码提交都自动触发测试。

7.1 整体流程

因为Host-based测试在PC上运行,所以CI环境不需要任何特殊硬件。一台普通的Linux/Windows CI服务器就够了。

7.2 GitHub Actions 示例

如果你的项目托管在GitHub,配置起来非常简单:

# .github/workflows/unit-test.ymlname: Unit Testson: [push, pull_request]jobs:  test:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - name: Install dependencies        run: sudo apt-get install -y cmake gcc      - name: Build tests        run: |          cd test          mkdir build && cd build          cmake ..          make      - name: Run tests        run: |          cd test/build          ctest --output-on-failure

每次push或提交PR时,GitHub Actions会自动编译并运行所有测试。测试不通过,PR会被标记为失败,团队成员一眼就能看到。

7.3 关键建议

  • • 测试要快。 Host-based的单元测试通常在秒级完成,这是它最大的优势。如果你的测试跑了好几分钟,说明要么不是单元测试,要么测试写法有问题。
  • • 测试要独立。 每个测试用例之间不应该有依赖关系,执行顺序不影响结果。Unity的setUp()tearDown()就是用来保证这一点的。
  • • 失败要有明确信息。 使用Unity的断言宏,失败时会自动打印期望值、实际值和行号,方便定位问题。

八、嵌入式单元测试中的常见误区

在实践中,我见过不少团队在落地单元测试时踩坑。这里总结几个比较典型的误区。

8.1 "先把功能写完,测试以后再补"

这是最常见的想法,也是最致命的。代码写完再补测试,面临两个问题:一是代码结构可能根本不支持测试(硬件耦合太紧),二是你已经对代码的正确性有了主观信心,写测试的动力大幅下降。

正确的做法是:设计阶段就考虑可测试性,至少保证接口是可Mock的。 不要求先写测试(TDD在嵌入式场景下有时确实不太顺畅),但至少要做到代码写完之后能立刻写测试。

8.2 "我要测所有函数"

不需要。单元测试的精力应该集中在:

  • • 业务逻辑层的核心算法(温度换算、PID计算、协议解析、状态机跳转等)
  • • 出过bug的模块(线上出了问题,先补测试再修bug,防止回归)
  • • 边界条件复杂的函数(各种临界值处理、错误码返回)

不值得测试的:

  • • 简单的赋值、初始化函数
  • • 纯粹的硬件驱动层代码
  • • 只是转发调用的薄胶水层

8.3 "Host上测过了就等于Target上没问题"

Host-based测试验证的是逻辑正确性,但它不能保证代码在目标平台上的运行时行为完全一致。以下差异需要注意:

差异点
Host (PC)
Target (MCU)
int宽度
32位或64位
可能16位
字节序
通常小端
看具体芯片
对齐要求
宽松
可能有严格对齐要求
浮点行为
硬件FPU
可能是软浮点
栈大小
很大
非常有限

应对策略:

  • • 使用stdint.h中的固定宽度类型(uint8_tuint16_t等),避免裸int
  • • 在测试中显式覆盖字节序相关的边界情况
  • • 对齐问题在代码中用__attribute__((packed))或手动序列化来规避
  • • 关键代码在Host测试通过后,仍需在Target上做集成验证

8.4 "覆盖率越高越好"

覆盖率是个参考指标,但不是目标。80%覆盖率和100%覆盖率之间的差距,往往需要花三倍以上的精力去弥补,而这些边边角角的代码可能永远不会出问题。

我在实际项目中的经验是:核心逻辑模块争取80%以上的行覆盖率,整体项目做到50%-60%的逻辑层覆盖率,就已经是一个不错的状态了。


九、总结:一张图串起来

最后,把嵌入式单元测试落地的关键环节串在一起:

嵌入式单元测试不是什么高深的事情,它的核心就是一个朴素的道理:把能在PC上验证的逻辑,就在PC上验证掉。 不需要等硬件就绪,不需要手动烧录,不需要拿着万用表量信号。

当然,单元测试不能替代在板上的集成测试和系统测试。它解决的是"逻辑对不对"的问题,而不是"硬件行不行"的问题。但在嵌入式项目中,相当大比例的bug本质上就是逻辑错误,而这些错误完全可以在PC端、在开发阶段、在几秒钟内被发现。

如果你还没有在项目中尝试单元测试,不妨从一个最简单的模块开始。找一个纯逻辑的函数,写三五个测试用例,体验一下"改了代码,跑一下测试,几秒钟就知道有没有问题"的感觉。一旦尝到甜头,你会想把更多的模块纳入进来。


说到嵌入式软件的架构设计,单元测试只是其中一个切面。代码能测试,前提是代码分层清晰、模块边界明确、依赖方向合理。如果你在项目中也遇到过模块耦合严重、协议和业务缠在一起、状态逻辑混乱、现场问题难定位这些困扰,推荐看看我整理的**《嵌入式软件架构实战合集》**。这个合集从分层设计、接口抽象、协议解耦、状态机、事件驱动、RTOS任务模型到日志诊断体系,都有结合实际项目的详细讲解,帮你建立一套可落地的嵌入式架构设计思路。

合集链接:《嵌入式软件架构实战合集》

【往期推荐】

嵌入式C语言设计模式实战

嵌入式软件中:协议解析与业务该如何解耦?

嵌入式软件该如何分层设计?

嵌入式软件 OTA 升级方案!

嵌入式软件 :模块解耦

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-06-27 00:22:20 HTTP/2.0 GET : https://g.sjds.net/a/458828.html
  2. 运行时间 : 0.127073s [ 吞吐率:7.87req/s ] 内存消耗:4,594.42kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=0c28a2c6bc963ea6b2d414024b789491
  1. /yingpanguazai/ssd/ssd1/www/g.sjds.net/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/g.sjds.net/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/g.sjds.net/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/g.sjds.net/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/g.sjds.net/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/g.sjds.net/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/g.sjds.net/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/g.sjds.net/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/g.sjds.net/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/g.sjds.net/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/g.sjds.net/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/g.sjds.net/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/g.sjds.net/runtime/temp/8321bd4d2de6fe7dffb246d4ae0c61fd.php ( 12.06 KB )
  140. /yingpanguazai/ssd/ssd1/www/g.sjds.net/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000909s ] mysql:host=127.0.0.1;port=3306;dbname=g_sjds;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001204s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.001625s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000269s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000581s ]
  6. SELECT * FROM `set` [ RunTime:0.006824s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000593s ]
  8. SELECT * FROM `article` WHERE `id` = 458828 LIMIT 1 [ RunTime:0.000515s ]
  9. UPDATE `article` SET `lasttime` = 1782490941 WHERE `id` = 458828 [ RunTime:0.015642s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 65 LIMIT 1 [ RunTime:0.000313s ]
  11. SELECT * FROM `article` WHERE `id` < 458828 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000517s ]
  12. SELECT * FROM `article` WHERE `id` > 458828 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.002473s ]
  13. SELECT * FROM `article` WHERE `id` < 458828 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000654s ]
  14. SELECT * FROM `article` WHERE `id` < 458828 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.006035s ]
  15. SELECT * FROM `article` WHERE `id` < 458828 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.006656s ]
0.128743s