C 语言书

单片机实践

把基础语法放进计数、按键、采样和温控场景中。

依据《单片机C语言基础学习_最新版》整理 · 12 个主题 · 每节约 6–12 分钟

先确认运行环境

标明“桌面示例”的完整程序可在 C 编译器运行。其他片段用于理解逻辑,硬件函数、端口与启动代码由目标工程提供,不可直接烧录。

  1. 变量与常量:计数器
  2. 运算符:条件与赋值
  3. if:温控输出完整赋值
  4. switch:按键选择模式
  5. for:清空显示缓冲区
  6. while:等待需要退出条件
  7. break / continue:筛选任务
  8. 数组:采样数据与查找表
  9. 函数:输入、输出与范围
  10. 宏定义:名称表达硬件含义
  11. 位运算:只修改目标位
  12. 指针:通过地址修改变量

温控边界实验

规则:温度 ≤ 20 ℃ 加热,温度 > 30 ℃ 散热,其余待机。先预测,再验证边界值。

风扇 · 关加热器 · 关待机
25 > 30 → 假;25 <= 20 → 假
fan = 0; heater = 0;
刚好 30 ℃ 时,哪个输出应该开启?

01 · 变量与常量:计数器

计数、温度和按键状态都是变量。使用前赋初值,并根据范围选择类型。

unsigned char cnt = 0;
int sum = 0;
cnt = cnt + 1;
sum = sum + cnt;

执行后 cnt = 1,sum = 1。0~255 以 8 位 unsigned char 为前提;实际范围可查看 limits.h 中的 UCHAR_MAX。

动手验证:初始 cnt 改为 9,执行后两个变量分别是多少?

查看参考结果

cnt = 10,sum = 10。长期累计还应估算最大值,避免计数回绕。

02 · 运算符:条件与赋值

算术得到数值,比较与逻辑运算得到真假。先判断条件,再执行赋值。

int a = 5, b = 3;
int c = a + b;
if (a > b && b > 0) {
    c = a - b;
}

c 先是 8,条件成立后变成 2。== 比较,= 赋值;& 不等于逻辑与 &&。

动手验证:把 b 改为 0,最终 c 是多少?

查看参考结果

c = 5。b > 0 为假,分支不执行。

03 · if:温控输出完整赋值

温度决定风扇与加热器状态。每次给两个输出赋值,避免上一次状态残留。

void control(int temp, unsigned char *fan,
             unsigned char *heater)
{
    *fan = 0;
    *heater = 0;
    if (temp > 30) {
        *fan = 1;
    } else if (temp <= 20) {
        *heater = 1;
    }
}

普通变量逻辑示例,不直接驱动 GPIO。调用时传入两个有效变量的地址。原文部分分支未重置另一输出,此处已修正。真实温控还需故障保护和回差。

动手验证:依次输入 15、35、25 ℃,每次 fan / heater 是多少?

查看参考结果

依次为 0/1、1/0、0/0。20 ℃ 加热;30 ℃ 不开启风扇。

04 · switch:按键选择模式

按键编号是离散值,适合 switch。每个分支用 break 防止意外继续执行。

switch (key) {
    case 1: step = 1; break;
    case 2: step = 2; break;
    default: step = 0; break;
}

key、step 需事先声明。这里只处理已识别的按键编号;扫描、去抖和有效电平取决于硬件。

动手验证:key = 2 与 key = 9 时,分别得到什么模式?

查看参考结果

分别是 step = 2 与 step = 0。default 为未知输入提供明确结果。

05 · for:清空显示缓冲区

已知次数的重复操作用 for,把下标限制在数组范围内。

unsigned char display_buffer[10];
unsigned char i;
for (i = 0; i < 10; i++) {
    display_buffer[i] = 0;
}

执行 10 次,最后访问下标 9。变量声明放在循环前,也便于阅读传统 C 编译环境的代码。

动手验证:如果条件误写为 i <= 10,会发生什么?

查看参考结果

会访问不存在的 display_buffer[10],发生越界。

06 · while:等待需要退出条件

while 适合次数不确定的重复,do-while 至少执行一次。无期限等待会阻塞后续任务。

unsigned int tries = 0;
while (flag == 0 && tries < 100) {
    wait_ms(1);
    tries++;
}
if (flag == 0) {
    report_timeout();
}

flag、wait_ms 和 report_timeout 由具体工程提供,片段不能单独编译。100 次仅为重试上限,不保证精确 100 ms;中断共享变量还需考虑 volatile 和原子性。

动手验证:flag 初始为 1,wait_ms 执行几次?

查看参考结果

0 次。do-while 则先执行一次循环体再判断。

07 · break / continue:筛选任务

break 结束当前循环;continue 跳过当前一轮剩余代码,只影响所在那一层循环。

unsigned char i;
for (i = 0; i < 100; i++) {
    if (i == 50) break;
    if (i % 2 == 0) continue;
    work(i);
}

work 是工程提供的函数。传入的是 1、3、5……49,遇到 50 循环结束。

动手验证:work 共调用几次?会处理 50 吗?

查看参考结果

共 25 次,不处理 50,因为调用前已经 break。

08 · 数组:采样数据与查找表

数组把同类型数据放在一起,读写前先确认下标范围。

unsigned char table[5] = {10, 20, 30, 40, 50};
unsigned char x;
table[0] = 99;
x = table[2];

table[0] 是第一个元素,table[2] 是第三个。修改第一个元素不影响第三个。

动手验证:这 5 个元素相加是多少?

查看参考结果

99 + 20 + 30 + 40 + 50 = 239。累计采样值时应选择容量足够的类型。

09 · 函数:输入、输出与范围

一个函数完成一件事。小类型相加也可能产生更大的结果,避免截断返回值。

#include <stdio.h>

unsigned int add(unsigned char a, unsigned char b)
{
    return (unsigned int)a + (unsigned int)b;
}

int main(void)
{
    printf("%u\n", add(200, 100));
    return 0;
}

面向常见 8 位 unsigned char 环境的桌面示例,输出 300。返回类型改为 unsigned int;桌面程序用 int main(void),不照搬特定嵌入式环境的 void main。

动手验证:8 位 unsigned char 环境中,若仍用 unsigned char 返回,300 变成多少?

查看参考结果

按 256 取模得到 44。这是无符号转换,不应当作期望结果。

10 · 宏定义:名称表达硬件含义

用名字代替魔法数字。数值常量和硬件别名要分清,宏本身不会创建变量。

#define MAX_COUNT 100
#define LED_ON 0
#define LED_OFF 1

unsigned int cnt = 101;
if (cnt > MAX_COUNT) {
    cnt = 0;
}

LED_ON = 0 只是假设低电平有效,须核对电路。原文 LED_PORT 对 P1 的别名仅适用于存在 P1 定义的目标环境。

动手验证:cnt = 100 会清零吗?

查看参考结果

不会,因为判断是 > 而不是 >=。定义宏时末尾不加分号。

11 · 位运算:只修改目标位

设置、清零、翻转用不同操作。把掩码写成二进制,观察目标位与其他位。

unsigned char reg = 0;
unsigned char hi;
reg |= 0x01;
reg &= (unsigned char)~0x02u;
reg ^= 0x04;
hi = reg >> 4;

结果 reg = 5(00000101),hi = 0。reg 是普通变量,不是真实寄存器;真实寄存器可能有写 1 清零、只读等语义,须查芯片手册。

动手验证:从 reg = 0xFF 开始执行三步,结果是多少?

查看参考结果

0xFF → 0xFF → 0xFD → 0xF9。高 4 位为 0x0F。

12 · 指针:通过地址修改变量

指针保存地址。先指向有效对象,再解引用读取或修改。

unsigned char v = 5;
unsigned char *p = &v;
unsigned char y = *p;
*p = 9;

y 保存当时读到的值 5,不是对 v 的持续引用。*p = 9 后,v 为 9,y 仍为 5。

动手验证:此时执行 y = *p,y 是多少?能解引用未初始化的指针吗?

查看参考结果

y 变为 9。不能解引用未初始化或无效指针,本例不需要猜测内存地址。