一、课程导论与测试环境搭建

各位同学好,我是老张。做嵌入式软件这行十几年了,踩过的坑比写过的代码还多。今天咱们开始聊窗帘电机嵌入式软件的单元测试,说实话,这个话题我早就想好好讲讲。

你可能觉得,一个窗帘电机嘛,不就是正转反转、停一停,有什么好测的?嗯,我当年也这么想。直到有一次,客户反馈说窗帘半夜自己开了——排查了三天,最后发现是一个全局变量被中断服务程序意外修改了。从那以后,我再也不敢小看单元测试了。

1.1 窗帘电机嵌入式软件测试概述

窗帘电机看起来简单,其实里面的门道不少。我给大家拆解一下:

  • 电机控制逻辑:正反转切换、堵转检测、行程计算
  • 传感器数据采集:霍尔传感器、限位开关、电流检测
  • 通信协议处理:通常是串口或者私有协议,处理各种指令
  • 状态机管理:待机、运行、停止、故障等状态切换
  • 定时与中断处理:PWM调速、定时器中断、外部中断

你想想看,这些模块任何一个出问题,轻则窗帘卡住不动,重则电机烧毁。我在项目里见过最离谱的bug,是限位开关的消抖延时写错了,导致窗帘每次到位后都会反弹两厘米。用户还以为是什么智能防夹功能呢。

核心观点:嵌入式软件测试不是可有可无的,它是产品质量的最后一道防线。尤其是窗帘电机这种消费级产品,出了问题直接影响用户体验。

1.2 单元测试概念

单元测试,说白了就是把你代码里最小的可测试单元拿出来,单独验证它对不对。这个"最小单元"通常是一个函数,或者一个模块。

我个人的习惯是:写一个函数之前,先想好它的测试用例。这不是什么高大上的方法论,纯粹是被坑出来的经验。有一次我写了一个计算行程的函数,自认为逻辑完美,结果测试发现边界条件少处理了一个——电机走到头了还在继续转,差点把窗帘扯坏。

单元测试要测什么?我给大家列个清单:

  • 正常路径:输入正常参数,看输出对不对
  • 边界条件:最大值、最小值、空值、零值
  • 异常处理:参数错误、状态异常、硬件故障
  • 状态转换:状态机在各种输入下的跳转是否正确
  • 时序逻辑:延时、超时、中断嵌套等场景

小提示:别想着一次把所有测试都写完。我建议你先写核心功能的测试,然后逐步补充。一口吃不成胖子,测试也是迭代出来的。

1.3 测试工具链介绍:Ceedling/Unity/CMock

好,到了大家最关心的部分——用什么工具。我推荐一套组合拳:Ceedling + Unity + CMock。这套工具链我在好几个项目里用过,稳定可靠,而且免费。

工具 作用 我的评价
Unity 单元测试框架,提供断言、测试用例管理 轻量级,C语言原生支持,没有花里胡哨的东西
CMock 自动生成Mock对象,用于模拟依赖模块 省去了手写Mock的繁琐工作,谁用谁知道
Ceedling 构建工具,把Unity和CMock整合在一起 一键运行测试,自动生成报告,省心

为什么会选这套工具?我简单说说理由:

  • 纯C语言,不挑编译器,GCC、ARMCC都能用
  • 不需要复杂的配置,几分钟就能跑起来
  • 社区活跃,遇到问题网上基本都能找到答案
  • 生成的可执行文件很小,适合资源受限的环境

注意:CMock生成Mock对象时,对函数指针和回调函数的支持有限。如果你项目里大量使用函数指针,可能需要手动写部分Mock。我在一个电机控制项目里就遇到过这个问题,最后是手写了几个关键Mock才搞定。

1.4 开发环境搭建:VS Code + GCC + Makefile

环境搭建这块,我推荐用VS Code + GCC + Makefile的组合。为什么?因为这套环境在Windows、Linux、macOS上都能用,而且完全免费。

我来手把手带大家搭一遍:

第一步:安装VS Code

去官网下载安装就行,记得装上这几个插件:

  • C/C++(微软官方那个)
  • Makefile Tools
  • Code Runner(可选,方便快速测试)

第二步:安装GCC工具链

Windows用户建议用MinGW-w64,Linux用户直接用系统自带的gcc。我个人习惯用MinGW-w64,因为它在Windows下表现最稳定。

第三步:安装Ruby

Ceedling是用Ruby写的,所以需要装Ruby环境。Windows用户去官网下载安装包,一路下一步就行。

第四步:安装Ceedling

打开命令行,执行:

gem install ceedling

等几分钟,安装完成。你可以用 ceedling version 验证一下。

第五步:创建项目

在VS Code终端里执行:

ceedling new curtain_motor_test

这会生成一个项目模板,目录结构是这样的:

curtain_motor_test/
├── src/          # 放你的源代码
├── test/         # 放测试代码
├── lib/          # 放第三方库
├── build/        # 编译输出
└── project.yml   # 配置文件

第六步:配置project.yml

打开project.yml,我一般会这样配:

:project:
  :build_root: build
:paths:
  :source:
    - src/**
  :test:
    - test/**
  :include:
    - src/**
:tools:
  :test_compiler:
    :executable: gcc
    :arguments:
      - -I"$": COLLECTION_PATHS_TEST_TOOLCHAIN_INCLUDE
      - -I"$": COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE
      - -c "${1}"
      - -o "${2}"
  :test_linker:
    :executable: gcc
    :arguments:
      - "${1}"
      - -o "${2}"
      - -lm

我的经验:第一次配置时,别急着改太多。先用默认配置跑一下 ceedling test:all,看看能不能通过。通了再慢慢调。我曾经花了一下午折腾配置,最后发现是路径写错了——这种低级错误,谁还没犯过呢?

第七步:写第一个测试

在test目录下创建一个文件 test_motor_control.c

#include "unity.h"
#include "motor_control.h"

void setUp(void) {
    // 每个测试用例执行前调用
}

void tearDown(void) {
    // 每个测试用例执行后调用
}

void test_motor_start_should_set_state_to_running(void) {
    motor_start();
    TEST_ASSERT_EQUAL(MOTOR_STATE_RUNNING, motor_get_state());
}

void test_motor_stop_should_set_state_to_idle(void) {
    motor_start();
    motor_stop();
    TEST_ASSERT_EQUAL(MOTOR_STATE_IDLE, motor_get_state());
}

然后在终端运行:

ceedling test:all

如果一切顺利,你会看到绿色的PASS字样。那一刻,说实话,挺有成就感的。

总结一下:环境搭建其实不难,关键是耐心。我第一次搭Ceedling环境时,光Ruby版本就折腾了两天。后来发现是路径里有中文空格——嵌入式开发者的日常,不是吗?

好了,这一章的内容就到这里。下一章我们会深入Unity的断言用法,教大家怎么写真正有用的测试用例。记住,测试不是为了测试而测试,是为了让你的代码更健壮,让你的产品更可靠。

有什么问题,欢迎随时交流。咱们下章见。