SMARTWATCH 学习手册有 C 基础 · 嵌入式从零开始
主线课程 / 学 GPIO 前的准备课

起点:让电路、代码与工具对上号

已经会写 C,但还没接触过单片机?先认识程序将在哪里运行、引脚怎样连接外部电路,以及学习时会用到哪些工具,再去点亮第一盏灯。

前置:C 的基本语法与函数不要求已有电路或单片机经验本节不需要烧录
这一节先解决什么?先建立一张简单的地图:电脑负责准备程序,单片机运行程序,引脚连接电路。暂时不用记寄存器、时钟树和链接脚本;后面真正需要它们时,再解释它们解决什么问题。

第 1 节 · 单片机是什么?开发板又是什么?

以前在电脑上运行 C 程序,输入可能来自键盘,结果可能显示在终端。现在想用程序控制一盏灯、读取按钮,程序就需要与这些电路连接。单片机正适合做这类工作:它是一颗能执行程序、保存数据,并通过引脚连接外部电路的芯片。

1.1 先认识芯片里的三个部分

部分先用一句话理解与 C 程序的联系
CPU按指令执行工作的部分执行运算、判断和函数调用
存储器保存程序和数据的地方程序需要存放,变量运行时也需要空间
连接外部的硬件功能帮助芯片读取或控制外部电路后面将通过库函数使用它们

把这些功能做在同一颗芯片里,就是“单片机”这个名字可以先传达的意思。以后看到 MCU,可以先把它理解为这里说的微控制器,也就是单片机。

1.2 芯片与开发板不是同一个东西

一颗芯片不能只放在桌上就运行:还需要正确供电,以及让它正常启动所需的电路。开发板把芯片和这些基础电路装在一块板子上,通常还提供便于连接的插针,有些板子自带灯、按钮和调试器。

所以,STM32 是一类芯片的名字;某块 STM32 开发板则是一件包含芯片和周边电路的实物。板上的 USB 插座、灯和按钮不一定直接属于芯片,具体连接要看这块板的说明。

开发板包含单片机和周边电路
从整块板子逐层找到芯片与外部电路;这不是所有板子的固定配置。

手表主板也是围绕芯片设计的一块电路板,只是连接了屏幕和传感器等更多器件。入门先用简单实验理解一个功能,再回到手表追踪同类功能,阅读负担会小很多。

第 2 节 · 电脑上的 C 文件,怎样变成芯片运行的程序?

在电脑里保存了 main.c,并不会让板子立刻执行它。芯片不能直接把 C 源码当成指令运行,需要先把源码转换成它能执行的程序,再写到芯片里。

2.1 写代码、构建、烧录、运行是四件事

从 C 源码到单片机运行的四步
这是一条常见开发流程。图中没有要求现在连接设备或执行烧录。

编译负责将源码转换为目标代码;一个工程通常不止一个源文件,链接再将需要的代码和数据组织成程序。开发工具里的 Build 往往会按需要完成这些步骤,本教程把这一整个过程称为“构建”。

烧录是把构建出的程序写入芯片的程序存储区;工具里也可能写作 Download 或 Program。这里的 Download 不是从网站下载资料。写入成功以后,还需要让芯片开始运行,再观察实际功能。

2.2 程序为什么断电后还在,变量却常常回到初始值?

先认识两种常见存储:Flash 常用来保存程序,断电后内容通常还在;RAM 用来存放运行时的数据,断电后不能继续保留原来的内容。程序重新启动时,普通变量会按启动流程重新准备。

这里先区分它们的用途,不需要现在计算各段地址。保存设置、掉电记录等功能,要到后面学习专门的数据保存方法。

怎样理解“构建成功”?它表示这次源码处理通过了相应构建步骤,不代表程序已经写入芯片,也不代表灯已经接对。比如代码里选错了 LED 引脚,程序仍可能构建成功,运行时却看不到预期灯光。

第 3 节 · 引脚、电压、高低电平,分别是什么?

3.1 引脚是芯片与电路连接的位置

芯片周围用来连接电路的接点叫引脚。部分引脚用于供电,部分用于连接外部信号。并非每个引脚都能随意拿来控制灯;读板子说明时,要分清电源、地和信号。

STM32 中常见 PA5 这样的信号引脚名称:P 可以先理解为端口引脚,A 表示 A 组,5 表示组内 5 号。它不是说芯片封装上从某个角数起的第 5 条腿。板子插针旁的编号也可能使用另一套名称,接线前要对上对应关系。

3.2 电压要相对于一个参考点说

板子上常能看到 GND,表示电路的地。在本教程的简单低压实验中,把它作为电压参考点。说“某信号是 3.3 V”,通常是说该信号相对于对应 GND 的电压约为 3.3 V。

想象用万用表测电压:黑表笔接参考地,红表笔接测量点,读到的是这两个点之间的电压。因此两块设备连接信号时,还要确认它们按要求连接了参考地,不能只看一根信号线。

3.3 高低电平怎样与 1、0 对应?

数字电路把一定范围内的电压判断为低电平,把另一个范围内的电压判断为高电平。在本教程常见的正逻辑约定中,低对应 0,高对应 1。比如在一个 3.3 V 实验电路里,可以先用“接近 0 V 的低”和“接近 3.3 V 的高”建立直观印象。

真实芯片判断的是电压范围,不要求每次恰好等于 0.000 V 或 3.300 V;具体范围由器件规定。电压落在不保证判断结果的中间范围,也不意味着得到一种可用的“第三个数字”。下一章先使用明确的高、低两种状态。

变量数值与引脚电平需要通过硬件操作联系起来
图中说明的是控制思路。具体函数、引脚模式和 LED 接法将在 GPIO 章逐步介绍。

例如写 int led = 1; 只是在程序里存了一个整数,芯片不知道变量名 led 对应哪盏灯。要改变真实引脚,需要指定硬件对象并使用相应操作。下面一章就从这个缺口开始讲。

接线前先确认这几件事3.3 V、5 V 标记不能混用;信号引脚也不是任意电源输出。先按实验板说明确认供电、引脚对应关系和 LED 限流电路,再连接。不能因为两个插针形状一样,就认为功能和允许电压相同。

第 4 节 · 开发工具分别帮助我们做什么?

不需要第一次就学会所有工具。先把“写程序”“把程序写进去”“与运行中的程序交换数据”区分开,后续才容易知道问题发生在哪一段。

工具做什么这时先认识到哪里
STM32CubeIDE在电脑上编辑、构建并调试 STM32 程序的开发环境IDE 是把多种开发功能放在一起的软件;先会找到工程和构建结果
STM32CubeMX通过界面选择引脚和外设参数它帮助准备初始化配置,应用功能仍要编写;具体选项随实验讲解
调试器,例如 ST-LINK连接电脑与芯片,支持程序写入、暂停和观察有些开发板已集成,有些需要外接
串口模块与串口工具让电脑与设备通过串口交换数据到 UART 章再接线和设置;普通 USB 转串口模块不等同于 ST-LINK
万用表测量电压等电气量第一次先按板子说明确认电源与 GND,测量方法需要选对挡位
逻辑分析仪、示波器观察随时间变化的信号先知道它们能帮助观察高低变化,通信和时序实验中再学习使用

4.1 “调试”是怎样看见程序正在做什么的?

程序运行很快,肉眼看不到它执行到哪一行。调试工具可以让它在指定位置暂停,这个暂停位置叫断点。暂停后可以查看变量,再让程序继续,或者一步步执行,这叫单步。

现在先理解这些动作的用途:如果灯不亮,可以检查程序是否执行到控制灯的位置。具体怎样启动调试、在哪里设置断点,等有明确实验目标时再操作。暂停也会改变运行时序,不能把暂停时观察到的现象直接当作正常连续运行结果。

4.2 同样插着 USB,功能可能不同

板上一个 USB 接口可能用于供电,也可能连接调试器、串口转换电路,或直接连接单片机的 USB 功能;具体由板子设计决定。所以“电脑已插 USB”不能单独证明程序已经烧录或串口已经连通。先查接口旁标记与开发板说明,再判断它承担什么工作。

第 5 节 · 已经有 C 基础,进入 HAL 工程先看哪里?

阅读顺序:HAL 库 → 常见整数类型 → 工程中的文件位置。

5.1 库替我们完成哪些工作?

如果每次点灯都要从芯片内部控制细节开始写,第一步就会很长。ST 提供了一套叫 HAL 的 C 库,英文全称 Hardware Abstraction Layer,可以理解为硬件抽象层。它把许多硬件操作包装成函数,应用按接口调用这些函数。

本教程先采用 HAL:先理解一个硬件功能是什么,再学习 CubeMX 中选什么,随后介绍函数作用、参数、返回值,最后把代码放进实验。寄存器是芯片内部保存控制与状态信息的位置;现在只认识这个名称,不要求先直接操作它。

5.2 uint8_t、int16_t 这些名字是什么意思?

进入 HAL 工程后,经常会看到 uint8_t,而不只是熟悉的 int。先从它能保存多少数据讲起:在本教程使用的 STM32 平台上,1 字节(byte)等于 8 位(bit)。每一位可以是 0 或 1,八位共有 256 种组合;从全 0 到全 1,如果都用来表示非负整数,范围就是 0~255。

uint8_t 正是这样的整数类型。把名字拆开看:

  • u:unsigned,表示无符号,不用来表示负数。
  • int:integer,表示整数。
  • 8:这个类型恰好占 8 位,在 STM32 上就是 1 字节。
  • _t:类型名常用的后缀,提示整个名字是一个类型。

所以 uint8_t data; 表示声明一个名为 data 的变量,它能保存 0~255。去掉开头的 u,得到 int8_t,它可以表示负数,但仍然只有八位,范围改为 −128~127。两者占用的空间相同,能表示的数值范围不同。

类型位数STM32 字节数能保存的整数范围常见用途
uint8_t8 位10~255一个通信字节、一个寄存器字节
uint16_t16 位20~65535超过 255 的非负计数
uint32_t32 位40~4294967295更大的计数、毫秒时间值
int8_t8 位1−128~127范围较小、可能为负的整数
int16_t16 位2−32768~32767可能为负的传感器原始值
int32_t32 位4−2147483648~2147483647需要更大正负范围的整数

这些类型由标准头文件 stdint.h 提供。先包含它,再按“类型名 变量名 = 初始值”声明变量;用法与以前的 int 变量一样,不需要自己实现这些类型。

普通变量声明示例 · 所有初始值都在对应类型的范围内

#include <stdint.h>

uint8_t register_byte = 0x68;
uint16_t sample_count = 1000;
uint32_t total_count = 1000000;

int8_t adjustment = -5;
int16_t signed_sample = -1200;
int32_t total_offset = -100000;

第一行变量保存十六进制 0x68,也就是十进制 104,能装进 uint8_t。sample_count 要保存 1000,已经超过 255,因此选 uint16_t;total_count 要保存一百万,选 uint32_t。下面三行要保存负数,分别选择范围够用的有符号类型。

熟悉的 int 有多宽,由目标平台与编译器约定;本项目 STM32 工具链中的 int 为 32 位,不能把这个结论直接套到所有平台。uint16_t、int32_t 这类名字则明确约定 16 位、32 位,适合在读写固定字节数时表达清楚数据宽度。

选类型时,先判断要不要负数,再判断最大、最小值是否放得下。uint8_t 装不下 1000,遇到这种情况,就应换成范围够用的类型。

5.3 第一次打开工程,只找这几个位置

位置先关注什么此时不用做什么
Core/Src/main.c找到 main 与已有初始化流程,再找到主循环不用从第一行起把整个文件读透
Core/Inc/应用头文件通常放在这里,用来提供声明不用把库的类型全部抄进自己的文件
Drivers/本项目所用的芯片支持代码与 HAL 库先通过接口使用,不直接改第三方库来点灯
SmartWatch.ioc本工程的 CubeMX 配置文件不要把改配置与已更新应用逻辑混为一件事

主循环常写作 while (1):条件一直成立,就不断执行循环体。很多入门裸机程序在完成初始化后,靠这个循环反复检查输入、更新输出。这里的“裸机”可以先理解为暂时不引入操作系统来安排任务,后面再学习更复杂的组织方式。

教程中的代码会说明放在文件作用域、初始化之后还是主循环里;不同位置作用不同。看到一个新 HAL 函数时,先读它前面的接口说明,不需要提前猜类型和参数。手表现有主程序已经包含多项功能,学习实验使用独立基础工程,避免直接用一个点灯循环替换现有应用。

main 前面还有程序吗?

有。芯片启动后,会先执行启动相关代码,准备运行环境,再到 C 程序的 main。这部分涉及启动文件、存储安排等内容。第一次先沿已经能运行的基础工程学习;向量表、启动和内存细节在后续需要时展开,不作为本章的阅读门槛。

第 6 节 · 回到项目:手表由哪几部分协作?

先认识分工,不要求现在读懂三个程序:

部分负责的主要工作以后在哪里学习
STM32U575 主控连接屏幕和传感器,运行设备侧应用GPIO 等主线课程,以及 LVGL、AI 支线
CH582M 通信芯片承担手表与手机之间的蓝牙通信,并与主控交换数据UART、协议与无线通信课程
手机 App在手机上显示和操作设备相关功能App 开发支线

它们运行在不同硬件上,不是把一个 main 函数分别改名。先从 STM32 端最简单的引脚输入输出学起;接着学习芯片之间怎样传递字节,之后才把蓝牙和手机端接进来。

想先看目录时,可以浏览 STM32 固件说明、CH582M 固件说明和 App 说明。第一次只看各自用途,不把完整手表工程的构建和运行当作学习 GPIO 的先决条件。

学 GPIO 前,做一次准备与自查

暂时没有设备,也能完成的部分

  1. 用自己的话区分单片机与开发板,指出程序实际在哪颗芯片上执行。
  2. 把“编辑源码、构建、烧录、运行观察”按顺序写出来,说明每一步得到什么。
  3. 在开发板说明或引脚图上寻找 GND、电源和信号引脚,区分引脚名称与排针序号。
  4. 在工程中找到 main.c 和主循环即可,暂时跳过还未学过的外设初始化细节。

具备独立实验板以后,再完成的部分

记录开发板型号、芯片型号、供电方式、LED 和按钮的实际接法,并确认所用基础工程能够运行。不要默认所有板子的灯都接 PA5、按钮都接 PB0;后续教程使用这些名称时,会明确它们是独立实验的约定。

下一章的第一个问题已经知道 C 变量不会自动连接一盏灯。接下来认识 GPIO:怎样让指定引脚输出高低电平,再通过实际电路让 LED 亮或灭;之后再反过来读取按钮。

检查理解

在 C 程序里写 int led = 1;,能直接保证板上的 LED 点亮吗?

参考与继续阅读

  1. ST · GPIO 硬件配置 AN4899
  2. ST · STM32U5 HAL 官方源码
  3. C11 委员会草案 N1570:§7.20.1.1 固定位宽整数类型、§7.20.2 数值范围,供查阅。

参考用于核对概念和接口;不同系列、库版本与开发板的参数不可直接互换。课程按本仓库现状说明项目,完整资料索引见 来源与版本。