Linux-下-STM32f103c8-开发板开发技巧

参考

这里以 Archlinux 为例, 主要需要安装的工具有:

  • keil 5
  • arm-none-eabi-binutils, “arm” 指该软件包针对 ARM 架构, “none” 表示该软件包不依赖特定的操作系统或库, “eabi” 为 Embedded Application Binary Interface, 即嵌入式应用的二进制接口, “binutils” 表示提供一系列工具集
  • arm-none-eabi-gcc, 针对 ARM 的 gcc 编译器
  • arm-none-eabi-gdb, 针对 ARM 的 gdb 调试器
  • stlink-utils, 与 ST-Link 调试器相关的工具集, 相关使用方法在另一篇文章

工具的安装方法这里暂时不做讲解. keil 5 主要是用他的函数库以及 build 出 AXF 文件. 主要的编辑工作还是在 neovim 中完成.

相关概念

AXF 文件

AXF (ARM eXecutable Format), 是一种用于 ARM 架构的可执行文件格式. 它是一种二进制格式, 用于存储与 ARM 目标设备相关的可执行代码, 数据和调试信息.

RCC

RCC, Reset and Clock Control, 是单片机中的一个模块, 用于控制系统的复位和时钟. 通常用于在系统启动时初始化系统的时钟设置.

GPIO

GPIO, General Purpose Input/Output, 是一种通用输入输出引脚, 可以在微控制器或单片机上配置为输入或输出引脚. 一般用于连接外部设备, 作为输出引脚时向外部设备发送信号, 控制其状态; 作为输入引脚时接收外部信号.

可能会遇到 “GPIOx”, 这是因为 STM32f103c8 开发板上有多组 GPIO 接口,

板子上的 A0, A1, A2 等就是 “GPIOA”, 而 B0, B1, B2 等则为 “GPIOB”, 以此类推.

OLED

OLED, Organic Light Emitting Diode, 有机发光二极管, 是一种功耗低, 响应速度较快的显示器. 一般在单片机开发中用于调试.

常用函数 (不同函数库也不同):

1
2
3
4
5
6
7
OLED_Init();
OLED_ShowChar(1, 1, 'A');
OLED_ShowString(1, 3, "Hello World");
OLED_ShowNum(2, 1, 12345, 5);
OLED_ShowSignedNum(2, 7, 12345, 5);
OLED_ShowHexNum(3, 1, 0xAA55, 4);
OLED_ShowBinNum(4, 1, 0xAA55, 16);

AHB 总线

AHB (Advanced High-performance Bus) 总线是一种在计算机系统中用于连接处理器, 内存和外设的总线架构.

AHB 总线被设计为高性能和高效的总线架构, 用于实现高速数据传输和协调不同部件之间的通信. 它提供了一种可靠的, 高带宽的数据传输通道, 以满足处理器和外设之间的数据交换需求.

外部中断 EXTI

EXTI, EXTernal Interrupt, 外部中断, 指通过外部引脚来触发中断事件. 当特定的外部事件, 如电平变化, 边沿触发等发生时, 单片机可以通过外部中断来及时响应并执行相应的中断程序.

具体为, EXTI 检测指定 GPIO 口的电平信号, 当其指定的 GPIO 口产生电平变化时, EXTI 将立即向 NVIC (后面有讲) 发出中断申请, 经 NVIC 裁决后中断 CPU 主程序, 执行对应的 EXTI 中断程序.

支持的触发方式有:

  • 上升沿, 电平从低到高的瞬间
  • 下降沿, 电平从高到低的瞬间
  • 双边沿, 电平从低到高或者从高到低都能
  • 软件触发, 不监听引脚, 有一行代码触发

需要注意, 所有的 GPIO 口都能用于触发中断, 但相同的 Pin 不能同时触发中断. (PA1, PB1, PC1 这些就属于相同 Pin, 即数字相同, 在 EXTI 的结构图中可以解释)

EXTI 的通道 (即哪些东西可以触发) 有: 16 个 GPIO_Pin, PVD 输出, RTC 闹钟, USB 唤醒和以太网唤醒.
EXTI 的基本结构如下:

可以看出从 GPIO 引脚到 EXTI 的只有 16 个通道, 而每一个 GPIO 引脚有 16 个引脚, 因此相同 Pin 不能同处用于触发 (这个时候就可以选出 16 个了).

AFIO, Alternate Function I/O, 其实就是 16 个并联的多路复用器:

触发中断后, 有两种响应方式:

  • 中断响应, 即申请中断, 让 CPU 执行中断函数
  • 事件响应, 此时中断信号不会通向 CPU, 而是通向其他外设, 用来触发其他外设的操作

有中断屏蔽寄存器和事件屏蔽寄存器用来 “屏蔽中断” (也就是不管这个中断信号).

除了外部中断 EXTI, STM32 还有很多类的中断, 具体可以上网搜, 这里不具体介绍.

中断优先级

当有多个中断源同时申请中断时, CPU 会根据中断的优先级来判断先执行哪一个中断程序.

中断嵌套

当一个中断程序正在运行时, 如果有新的更高优先级的中断源 (也就是触发中断的事件) 申请中断, CPU 会暂停当前中断程序, 转而去处理新的中断程序, 处理完后依次进行返回.

NVIC

NVIC, Nested Vector Interrupt Controller, 嵌套向量中断控制器.

NVIC 是 STM32 微控制器中用于管理和控制中断的关键组件. 它负责处理和分发中断请求, 并管理中断优先级, 中断向量表等.

其具体结构如:

可以看到, NVIC 有多个输入口, 可以连接多个中断线路, 这里线路上的 n 指一个外设可能会同时占用多个中断通道.

NVIC 只有一个输出口, 因此当多个中断同时申请时, 其会根据每个中断的优先级来分配中断的先后顺序, 让 CPU 执行.

NVIC 的中断优先级又分:

  • 抢占优先级, 高的可以中断嵌套
  • 响应优先级, 高的可以优先排队

NVIC 中的中断优先级存储在优先级寄存器中, 其有 4 位对应 0~15, 其中 0 的优先级最高. 由于要设置两类优先级, 其分配规则为 “若高的 n 位” 作为抢占优先级, 那么 “剩下的 4-n 位” 则作为响应优先级. 比如:

  • 抢占优先级用 0 位寄存器 (取值只有 0), 那么响应优先级就可以使用 4 位寄存器 (取值有 0~15)
  • 抢占优先级用 1 位寄存器 (取值只有 0~1), 那么响应优先级就可以使用 3 位寄存器 (取值有 0~7)
    等等组合.

旋转编码器判断方向的原理

旋转编码器, 是用于测量位置, 速度或旋转方向的装置, 当其旋转轴旋转时, 其输出端可以输出与旋转速度和方向对应的方波信号, 读取方波信号的频率和相位信息即可得知旋转轴的速度和方向.

常见的旋转编码器类别有:

  • 机械触点式
  • 霍尔传感器式
  • 光栅式

如图中的第一个就是一种光栅式旋转编码器, 配合对射式红外传感器 (一侧射出红外线, 一侧感应), 由于光栅的存在, 红外光会出现遮挡, 透过, 遮挡, 透过的现象 (遮挡时高电平, 透过时低电平). 因此会出现高低电平交替的方波:

方波的个数表示转过的角度, 方波的频率表示转速. 可以用外部中断来捕获方波边沿, 以此判断位置和速度. 但第一种无法判断方向.

第二三图是利用金属触点的旋转编码器, 在旋转时, 以此断开和导通两侧的触点, 由于金属盘的特殊设计, 其能够让两侧触点的通断产生一个 90 度的相位差 (假设两侧触点为 A, B).

当正转时, B 相输出的波形比 A 相滞后 90 度:

当反转时, B 相输出的波形比 A 相超前 90 度:

这样就能区分出方向, 而这种相位相差 90 度的波形就叫正交波形. 带正交波形的编码器一般都能用于测量方向.

还有编码器是利用: 一个引脚输出方波信号表示转速, 另一个引脚输出高低电平表示方向.

TIM 定时器中断

TIM, TIMer, 定时器. 其可以对输入的时钟进行计数, 并在计数值达到设定值时触发中断.

当输入时钟为基准时钟 (一般是主频 72MHz) 时, 计数器进行计数的过程, 实际上就是计时的过程 (记 72M 次就是 1s).

在 STM32 中, 计数器为 16 位, 每一次时钟则加一, 在 72MHz 计数时钟下可以实现最大 59.65s (72M/65536/65536, 分频器最大值 65536, 重装计数器设置最大目标值为 65536) 的定时.

16 位的计数器, 预分频器 (对输入时钟进行分频, 比如输入 72MHz, 可以设置预分频器值为 100, 此时输入的时钟频率则为 72MHz/100), 自动重装寄存器 (存储计数的目标值, 也就是记多少次进行中断) 一起组成时基单元.

STM32 计时器还支持级联模式, 即一个定时器的输出能当作另一个计时器的输入, 此时最大计时时间可以为 $59.65s \times 65536 \times 65536$ (过 59.65s 第二个计时器才记一次数, 由于分频 65536 次, 则 $59.65s \times 65536$ 之后记一次数, 由于目标计数最大可以是 65536, 则最长为 $59.65s \times 65536 \times 65536$).

STM32 的定时器, 除基本的定时中断外, 还包含内外时钟源选择, 输入捕获, 输出比较, 编码器接口, 主从触发等多种功能.

根据定时器的复杂度, 可以分为:

  • 高级定时器
  • 通用定时器
  • 基本定时器

这些定时器在库函数中的编号, 总线不同, 功能不同.

需要注意, 不同型号的 STM32, 定时器的数量是不同的, 在使用这个外设之前, 需要查看是否有这个外设.

定时中断的基本结构为:

  • 运行控制, 指控制寄存器的一些位, 包括启动停止, 向上或向下计数等, 可以控制时基单元的运行
  • 左侧是为时基单元提供时钟的部分
  • 有预分频器和预分频缓冲器两个, 后者保证在更新事件来临时调整时钟频率
  • 预分频器内部也是通过一个计数器来实现分频的

计数方式

对于基本定时器, 一般只有向上计数模式, 即数字从 0 自增到重装值, 然后置 0 并触发中断.

而对于通用定时器和高级定时器, 则有向下计数模式 (从重装值向下自减, 减到 0 后回到重装值并申请中断) 和中央对齐模式 (先从 0 向上自增到重装值申请中断, 然后从重装值向下自减到 0 申请中断).

时钟选择

对于基本定时器而言, 只能选择内部时钟来定时, 也就是系统频率 72MHz.

而通用定时器, 则不仅可以选择内部时钟, 还可以选择外部时钟, 如来自 TIMx_ETR 引脚的输入信号.

TRGO

TRGO, Timer Gated Reset Output, 定时器门控复位输出, 是一个定时器输出的选项. 当定时器满足一定的触发条件时, 就会从 TRGO 输出信号到其他外设执行操作.

可以用 TRGO 来级联定时器等.

主从触发模式

让内部的硬件在不受程序的控制下实现自动运行.

主从触发模式是选择 “主模式”, “从模式” 和 “触发源选择” 的简称:

主模式指, 将定时器内部的信号, 映射到 TRGO (Timer Gated Reset Output) 引脚, 用于触发别的外设.

从模式指, 通过接收其他外设或者自身外设的一些信号 (触发源选择来指定) 来控制自身定时器的运行 (从模式主要是选择定时器执行什么操作).

触发源选择指, 选择用什么信号源来触发从模式. (可以认为是从模式的一部分)

TIM 输出比较

OC, Output Compare, 输出比较. 其通过比较 CNT (Counter) 与 CCR (Compare/Capture Register) 寄存器值之间的关系, 来对输出电平进行置 1, 置 0 或翻转的操作, 用于输出一定频率和占空比的 PWM 波形.

CCR 寄存器是输入和输出共用的, 当输入时, 则用于 Capture, 当输出时, 则用于 Compare.

一般 CNT 值是自增, 而 CCR 是一个给定的值.

每个高级定时器和通用计时器都拥有 4 个输出比较通道, 也就是能同时输出四路 PWM 波形. 这四个通道有各自的 CCR 寄存器, 但是他们共用一个 CNT 计数器.

PWM

PWM, Pulse Width Modulation, 脉冲宽度调制. 宽度调制, 其实就是调整占空比. $T_{ON}/T_{Total}$

在具有惯性的系统中, 才能够用 PWM 进行调整, 比如电机的转速, LED 的亮度.

不同的占空比, 对应不同有效电压. 计算公式为:

1
有效电压 = Peak Voltage x 占空比

比如 PWM 输出方波的 Peak Voltage 为 2V, 当占空比为 1 时, 有效电压就为 $2 \times 1 = 2V$.

当占空比为 0.5 时, 有效电压为 $2 \times 0.5 = 1V$.

PWM 一般需要设置三个参数:

  • 频率
  • 占空比
  • 分辨率 (就是最小步长, 用于改变占空比, 比如 1% 就是占空比每次变化 1%)

利用这个电压有效值, 就可以用 PWM 通过输入数字量来输出模拟量.

输出比较的八种模式

REF 指 “REFerence”, 这里是输出的电平.

在 TIM 中, 产生 PWM 的基本结构为:

(这里蓝色线是 CNT 的值, 黄色线是 ARR 的值, CNT 的值从 0 开始自增, 到达 ARR 值时清零, 这里红色线是 CCR 的值, 在这里, 占空比相当于是 0.3, 也就是 $CCR/ARR+1$)

PWM 三个参数的计算分别为:

  • PWM 频率
    $$
    \displaylines
    {
    \begin{aligned}
    Freq = CK_PSC \times \frac{1}{PSC+1} \times \frac{1}{ARR + 1}
    \end{aligned}
    }
    $$
  • PWM 占空比
    $$
    \displaylines
    {
    \begin{aligned}
    Duty = \frac{CCR}{ARR + 1}
    \end{aligned}
    }
    $$
  • PWM 分辨率
    $$
    \displaylines
    {
    \begin{aligned}
    Reso = \frac{1}{ARR + 1}
    \end{aligned}
    }
    $$

舵机

舵机是一种根据输入 PWM 信号占空比来控制输出角度的装置. 如:

一般舵机的接线为:

直流电机

直流电机是一种将电能转换为机械能的装置, 有两个电极, 当电极正接时, 电机正转, 当电极反接时, 电机反转.

由于直流电机属于大功率器件, GPIO 口无法直接驱动, 需要配合电机驱动电路来操作. 常见的电机驱动芯片有 TB6612, DRV8833, L9110, L298N 等.

介绍下 TB6612 芯片, 其为双路 H 桥型 (H 桥型意味着一路有 4 个开关管, 可以控制正反转) 的直流电机驱动芯片, 可以驱动两个直流电机并且控制其转速和方向. 其硬件电路为:

注意, PWMA, AIN2AIN1 用于控制 AO1AO2.

PWMB, BIN2BIN1 控制 BO1BO2.

右下角的表解释了怎么控制电机的方向.

TIM 输入捕获

IC, Input Capture, 输入捕获, 是定时器的功能. 输入捕获模式下, 当通道输入引脚出现指定电平跳变 (上升沿, 下降沿等) 时, 当前 CNT (CouNT) 的值将被锁存到 CCR (Capture/Compare Register) 中, 可用于测量 PWM 波形的频率, 占空比, 脉冲间隔, 电平持续时间等参数.

每个高级定时器和通用定时器都拥有 4 个输入捕获通道. 而基本定时器没有输入捕获功能.

当配置为 PWMI 模式 (PWM Input 模式, 是专门为测量 PWM 频率和占空比设计的) 时, 可用于测量频率和占空比.

可以配合主从触发模式, 实现硬件全自动测量.

频率测量

对于 STM32 而言, 其只能测量高低电平组成的数字信号, 若想要测量正弦波, 还需要搭建一个信号预处理电路 (用运放搭建一个比较器, 把正弦波转换为数字信号, 再输入给 STM32, 还需要考虑隔离等).

有两种方法可用于测频率:

  1. 测频法, 在闸门时间 T (就是一定时间) 内, 对上升沿/下降沿计次, 得到 N, 那么频率为:
    $$
    \displaylines
    {
    \begin{aligned}
    f_x = \frac{N}{T}
    \end{aligned}
    }
    $$
    (适合测量高频信号, 结果更新慢, 也就是如果频率慢慢在变化, 这时测出来的频率变化较慢, 因为这里实际上测的是平均值)
  2. 测周法, 两个上升沿内, 以标准频率 $f_c$ (定时器记一个的时间) 计次, 得到 N (这里的 N 表示定时器记了多少次, 用来算时间, 也就是先求一个周期的时间, 然后取个倒数得到频率), 则频率为:
    $$
    \displaylines
    {
    \begin{aligned}
    f_x = \frac{f_c}{N}
    \end{aligned}
    }
    $$
    (适合测量低频信号, 因为此时计次能比较多, 误差比较小, 结果更新比较快, 因为测量一个周期就得出结果)

这里需要注意 中界频率 的概念, 即测频法与测周法误差相等的频率点, 示意如:

有:
$$
\displaylines
{
\begin{aligned}
f_m = \sqrt{f_C / T}
\end{aligned}
}
$$
(减小测频法与测周法的 $\pm 1$ 误差, 这里是两个 N 相等时计算得出)
这个 $f_m$ 用来决定是用测频法还是测周法, 当待测频率小于 $f_m$ 时, 用测频法误差更小, 当待测频率大于 $f_m$ 时, 用测周法误差更小.

输入捕获的基本结构

如图:

“TI1FP1” 指 “Timer Input 1 Filter, Prescaler and Prescaler”, 定时器输入触发极性选择位. 用于配置定时器的输入触发极性,以确定何时触发定时器的计数操作.

PWMI 基本结构

如图:

定时器编码接口

Encoder Interface, 编码接口.

对于需要频繁执行, 操作又比较简单的任务, 一般都会设计一个硬件电路模块. 来自动完成.

这里用定时器编码接口, 来自动给编码器进行计次的硬件模块.

编码器接口可接收增量 (正交) 编码器的信号, 根据编码器旋转产生的正交信号脉冲, 自动控制 CNT 自增或自减 (一个脉冲加1或减1), 从而指示编码器的位置, 旋转方向和旋转速度.

正交编码器一般可以测量位置, 或者带有方向的速度值, 一般有两个输出引脚, 一个是 A 相, 一个是 B 相.

正交信号脉冲指, A, B 相输出的两个方波信号, 相位相差 90 度, 超前 90 度或者滞后 90 度, 分别代表正转和反转.

对于带旋转轴的编码器, 一般转得越快, 这个方波的频率就越高 (也就代表速度, 此时取任意一相来测频率即可得到速度)

正交信号测速度和方向相比于单独输出一个高电平表示正转, 低电平表示反转的精度更高 (因为此时 A, B 相都可以用于计次, 且可以抗噪声)

每个高级定时器和通用定时器都拥有 1 个编码器接口. (一个定时器如果设置为编码器接口模式, 基本上就干不了其他事了)

定时器编码器接口有两个输入端, 分别接外设编码器的 A 相和 B 相:

编码器的两个输入引脚会借用输入捕获的 channel1 和 channel2.

编码器接口的输出部分, 相当于是从模式控制器.

编码器接口基本结构

ADC

ADC, Analog-Digital Converter, 模拟-数字转换器.

ADC 可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量, 进而建立模拟电路到数字电路的桥梁. (毕竟原本 STM32 的引脚只认识 3.3V (高电平) 和 0V (低电平))

STM32f103c8 的 ADC 的分辨率是 12 位, 工作模式为 “逐次逼近型”, 转换时间为 “1us” (从 AD 转换到得到结果的时间).

输入电压范围一般是引脚能接收的最低到最高, 如 0~3.3V, 转换结果的范围为 0~4095 (也就是 12 位)

STM32 的 ADC 有 18 个输入通道, 可测量 16 个外部 (也就是 16 个 GPIO 引脚) 和 2 个内部 (只内部的温度传感器和内部参考电压) 信号源.

还有规则组和注入组两个转换单元用于强化 ADC 功能. 普通的 AD 转换流程是, 启动一次转换, 读一次值, 然后再启动, 再读值. 而这里, 可以读取一个值后连续转换多个值. 规则组用于普通事件, 注入组用于突发事件.

也有模拟看门狗自动监测输入电压范围. (达到某个范围时触发某个行为) 其实际上监测某些通道, 当 AD 值高于或低于它设定的阈值时, 它就会申请中断, 此时可以在中断函数中执行相应的操作.

DMA

DMA, Direct Memory Access, 直接存储器存取. 这个外设可以直接访问 STM32 内部的存储器. 包括运行内存 SRAM, 程序存储器 Flash 和寄存器等.

DMA 可以提供外设 (指外设的数据寄存器 DR, Data Register, 比如 ADC 的数据寄存器, 串口的数据寄存器等) 和存储器 (指运行内存 SRAM 和程序存储器 Flash, 也就是存储变量数组和程序代码的地方) 或者存储器和存储器之间的高速数据传输, 无须 CPU 干预, 节省了 CPU 的资源.

一般的 STM32 芯片中, 有 12 各独立可配置的通道 (也就是数据转运的路径, 数据从一个地方移动到另一个地方, 就需要占用一个通道, 如果有多个通道进行转运, 那他们之间可以各转各的, 互不干扰), DMA1 (7 个通道), DMA2 (5 个通道).

每个通道都支持软件触发 (一般是存储器到存储器间的转运, 如将 SRAM 中的数据转运到 Flash 中, 就需要此触发) 和特定的硬件触发 (一般是外设到存储器的转运, 比如转运 ADC 的数据, 因为外设的转运需要一定时机, 如 ADC 的一次 AD 转换之后由硬件触发一次 DMA 来转运, 触发一次转运一次).

STM32f103C8T6 的 DMA 资源为: DMA1 (7 个通道).

STM32 中的存储器映像

STM32 中所有的存储器, 其作用和对应地址如下:

程序存储器 Flash 为主闪存 Flash, 而实际上系统存储器和选项字节 (主要存储 Flash 的读保护, 写保护, 看门狗的配置等) 也都是使用 Flash 介质.

外设寄存器实际使用的介质也是 SRAM.

寄存器算是软件和硬件之间的桥梁, 软件读写寄存器, 就相当于在控制硬件的执行.

DMA 仲裁器, 虽然 DMA 有多个通道, 但是 DMA 总线只有一条, 所以所有的通道都只能分时复用这一条 DMA 总线. 如果产生了冲突, 那就会由仲裁器, 根据通道的优先级来决定谁先谁后.

如果 DMA 和 CPU 都要访问同一个目标, 那么 DMA 就会暂停 CPU 的访问, 以防止冲突. 不过总线仲裁器, 仍然会保证 CPU 得到一半的总线带宽, 使 CPU 能正常工作.

DMA 基本结构

这里也标出了, 数据可以是 “外设寄存器” 和 “flash/SRAM” 双向转运, 也可以是 “flash” 到 “SRAM” 的转运.

传输计数器用于指定要转运几次数据, 每转运一次, 计数器的值减一. 当减到 0 之后, 便不会再进行转运, 且之前存储器中自增的地址, 也会恢复到起始地址的位置, 以方便 DMA 开始新一轮的转运.

自动重装器用于配置, 当计数器减到 0 之后, 是否要恢复到最初的值. 比如一开始计数器值为 5, 减到 0 之后, 自动重装器又将计数器的值设为 5. 如果不重装, 就是单次转运模式, 如果重装, 就是循环模式.

M2M, Memory to Memory, 决定是使用硬件触发还是软件触发. 这里的软件触发就是指迅速将传输计数器的值清零, 因此其和循环模式不能同时使用. (如果同时使用, DMA 就无法停止了). 硬件触发的触发源可以选择 ADC, 串口, 定时器等.

开关控制指 DMA_Cmd(), DMA 启动需要几个条件:

  1. 开关控制中, DMA_Cmd() 必须使能
  2. 传输计数器必须大于 0
  3. 触发源必须有触发信号

当要手动修改传输寄存器的值, 需要先 DMA_Cmd() disable, 再进行.

注意每一个通道支持的硬件触发源不同 (每个通道的软件触发源相同). 比如用 ADC1 触发, 就需要使用通道 1.

USART 串口协议

STM32 中支持的通信接口及其简单配置和参数大致有:

通信的目的: 将一个设备的数据传输到另一个设备, 扩展硬件系统 (与一些通信模块互联).

通信协议指: 指定通信的规则, 通信双方按照协议规则进行数据收发.

TX (Transmit eXchange), 数据发送脚.

RX (Receive eXchange), 数据接收脚.

SCL (Serial CLock), 时钟.

SDA (Serial DAta), 数据.

SCLK (Serial CLocK), 指时钟.

MOSI (Master Output Slave Input), 主机输出数据脚.

MISO (Master Input Slave Output), 主机输入数据脚.

CS (Chip Select), 片选, 用于指定通信的对象.

全双工指发送线路和接受线路互不影响, 可同时进行 (两根数据线).

半双工指同一时刻只能发送或接收数据 (一根数据线).

有单独的时钟线, 则是 “同步” 的, 发送和接收方在时钟信号的指引下进行采样.

没有单独的时钟线, 需要双方约定一个采样频率, 则是 “异步”, 同时需要添加一些帧头帧尾等, 进行采样位置的对齐.

单端信号通信的双方需要共地, 因为其引脚的高低电平都是对 GND 的电压差.

而差分信号是靠两个差分引脚的电压差来传输信号, 在通信时, 可以不需要 GND (可以极大控制抗干扰, 一般传输速度和距离都非常高).

点对点就是 1 对 1, 多设备就是 1 对 多, 通信时需要寻址选择设备.

串口通信

串口是一种应用十分广泛的通讯接口, 其成本低, 容易使用, 通信线路简单, 可实现两个设备的互相通信 (一般都是点对点).

单片机的串口可以使单片机与单片机, 单片机与电脑, 单片机与各种模块互相通信.

串口通信, Serial Communication, 即使用串行传输方式,将数据一位一位地按顺序发送和接收.

除串口外, 还有:

  • 并行通信, 多个数据位同时传输,每个数据位使用独立的通信线路, 更快地传输数据
  • 以太网通信, 使用包交换的方式传输数据
    等等.

串口通信的硬件电路

最简单的一般为:

一般都有四个引脚, 其中 TX 和 RX 交叉连接.

串口通信常用电平标准

电平标准定义了在串口通信中使用的电压级别 (即多大电压是高电平, 多大是低电平) 和信号极性 (大的电压是高电平还是小的电压是高电平) , 以确保发送方和接收方之间的正确数据传输, 常见的有三种:

  • TTL 电平 (Transistor-Transistor Logic), +3.3V+5V 表示 1, 0V 表示 0 (单片机中最常见)
  • RS232 电平, -3~-15V 表示 1, +3~+15V 表示 0
  • RS485 电平, 两线压差 +2~+6V 表示 1, -2~-6V 表示 0 (差分信号, 通信距离可以很长)

串口参数及时序

一些概念有:

  • 波特率, 串口通信的速率 (即约定的传输频率, 发送和接收都需要约定好), 其决定了没隔多久发送一位
  • 起始位, 标志一个数据帧的开始, 固定为低电平
  • 数据位, 数据帧的有效载荷, 1 为高电平, 0 为低电平, 低位先行 (如发送 0000 1111, 则从最右侧的 1, 即低位开始发送 )
  • 校验位, 用于数据验证, 根据数据位计算得来 (如奇偶校验, CRC 校验)
  • 停止位, 用于数据帧之间的间隔, 固定为高电平 (也就是为下一个起始位做准备)

结构如:

(左边的没校验位, 右边的添加了奇偶校验)
这里串口空闲的时候, 置高电平, 而开始时则先发送一个起始位, 即低电平, 来打破空闲状态的高电平, 产生一个下降沿, 告诉接收设备这一帧数据开始了.

在二进制调制下, 一个码元就是一个 bit. 此时波特率就等于比特率.

在 STM32 中停止位可以设置为 1, 1.5, 2, 0.5. “1” 的意思为, 其时间长度和一个数据位相同, “1.5” 的意思是长度为数据位的 1.5 倍.

USART 外设

USART, Universal Synchronous/Asynchronous Reveiver/Transmitter, 通用同步/异步收发器 (一般都用作异步通信).

USART 是 STM32 内部继承的硬件外设, 可根据数据寄存器的一个字节数据自动生成数据帧时序, 从 TX 引脚发送出去, 也可以自动接收 RX 引脚的数据帧时序, 拼接为一个字节数据, 存放在数据寄存器里.

其自带波特发生器, 即用于配置波特率的一个分频器, 比如 APB2 总线为 72MHz 的频率然后其进行分频, 得到我们想要的波特率时钟. 最后在这个时钟下, 进行收发 (也就是指定了波特率).

STM32f103c8T6 的 USART 资源有: USART1, USART2, USART3.

支持 “硬件流控制”, 比如, 设备 A 的 TX 向摄别 B 的 RX 发送数据, 但由于 A 设备发得太快, B 处理不过来, 若没有硬件流控制, B 只能抛弃新数据或者覆盖原数据, 如果有硬件流控制, 则在硬件电路上, 会多一根线, 如果 B 没准备好接收, 就置高电平, 如果准备好了, 则置低电平. A 接收到了 B 反馈的准备信号, 只有在 B 准备好时才会发送数据. 可以防止应为 B 处理慢而导致数据丢失的问题.

若流控有两个引脚 nRTS (Request To Send, 是请求发送, 输出脚, 告诉别的设备是否准备好), nCTS (Clear To Send, 清除发送, 是输入脚, 接收别的 nRTS 信号), 这里的 n 表示低电平有效.

USART 的基本结构

波特率发生器处的计算

发送器和接收器的波特率由波特率寄存器 BRR 里的 DIV 确定, 计算公式为:
$$
\displaylines
{
\begin{aligned}
波特率 = \frac{f_{PCLK2/1}}{16 \times DIV}
\end{aligned}
}
$$

I2C 通信

I2C, Inter-Integrated Circuit.

I2C 总线是一种通用的数据总线, 包括两条通信线: SCL (Serial Clock, 由主机控制), SDA (Serial Data).

I2C 通信的特点为同步通信以及半双工. 具有数据应答功能 (即接收到了数据会回传一个接收到了的 bit). 其也支持总线挂载多设备 (一主多从, 多主多从)

异步时序的好处在于可以节省一根时钟线的资源, 缺点就是对时间要求严格, 对硬件电路依赖比较严重.

同步时序的好处在于, 对时间要求不严格, 对硬件电路不怎么依赖, 在低端单片机没有硬件支持的情况下, 也容易使用软件来模拟时序. 缺点在于需要多使用一条时钟线资源.

典型的硬件电路

如:

可以看到, 所有 I2C 设备的 SCL 需要连在一起, SDA 需要连在一起. 且设备的 SCL, SDA 均要配置成开漏输出模式 (避免强上拉导致的电源短路).

对于 SDA 数据线, 从机不允许主动发起对 SDA 的控制, 只有在主机发送读取从机的命令后或者从机应答的时候, 从机才能短暂地获取 SDA 的控制权.

SCL 和 SDA 各需要添加一个上拉电阻, 阻值一般为 $4.7k \Omega$ 左右 (弱上拉).

I2C 时序基本单元

起始条件: 在 SCL 高电平期间, SDA 从高电平切换到低电平.

终止条件: 在 SCL 高电平期间, SDA 从低电平切换到高电平.


(注意起始和终止都是由主机产生的)

主机发送字节的过程为: 在 SCL 低电平期间, 主机将数据位放到 SDA 线上 (这里是高位先行), 然后释放 SCL, 从机在 SCL 高电平期间读取数据位, 因此在 SCL 高电平期间 SDA 不允许有数据变化. 重复 8 次后, 即可发送一个字节.

主机接收字节的过程为: 在 SCL 低电平期间, 从机将数据位放到 SDA 线上 (这里是高位先行), 然后释放 SCL, 主机在 SCL 高电平期间读取数据位, 因此在 SCL 高电平期间 SDA 不允许有数据变化. 重复 8 次后, 即可接收一个字节. (主机在接收之前, 需要释放 SDA, 即让从机能够控制 SDA)

由主机发送应答: 主机在接收完一个字节之后, 在下一个时钟发送一位数据, 数据 0 表示应答, 数据 1 表示非应答.

由主机接收应答: 主机在发送完一个字节之后, 在下一个时钟接收一位数据, 判断从机是否应答, 数据 0 表示应答, 数据 1 表示非应答. (主机在接收之前, 需要释放 SDA, 即让从机能够控制 SDA)

I2C 具体通信

这里用 I2C 的一主多从模型, 主机可以访问总线上的任何一个设备, 需要先把每个从设备都确定一个唯一的设备地址, 从机设备地址就相当于每个设备的名字, 主机在起始条件之后, 要先发送一个字节 (即从机的地址) 确定从机, 这里所有的从机都会收到这个字节, 与自己的地址进行比较, 如果不同, 则认为没有被主机选中, 之后的时序便不管, 若相同, 则需要响应之后主机的读写操作

从机设备地址, 在 I2C 协议标准中分为 7 位地址和 10 位地址, 这里考虑 7 位地址的模式.

在每个 I2C 设备出厂时, 厂商都会为它分配一个 7 位的地址, 具体可以在芯片手册中查询. 一般不同型号的芯片地址不同, 相同型号的地址相同. 当挂载多个相同型号的模块时, 则利用地址中的可变部分 (一般最后几位可以在电路中改变, 如由某几个引脚来改变)

第一个字节是从机地址 (高 7 位) 加上读写位 (最后 1 位).

接下来需要先读取一个应答位后才发送数据.

一般第二个发送的字节表示要写入的寄存器地址.

第三个发送的字节表示要写入的寄存器内的值.

这是指定地址写的时序, 即指定设备 (Slave Address), 在指定地址 (Reg Address) 下, 写入指定数据 (Data).

对于当前地址读的时序, 则为, 对于指定设备 (Slave Address), 在当前地址指针指示的地址下, 读取从机数据 (Data).

第一个字节发送从机地址 (高 7 位) 和读 (1 位), 在接收到应答后就开始读取数据

当从机中, 所有的寄存器被分配到了一个线性区域中, 并且会有一个单独的指针变量, 指示着其中一个寄存器, 这个指针在上电时一般默认指向 0 地址并在没写入和读出一个字节后, 该指针会自动自增一次移动到下一个位置. 这个指针即 “当前地址指针”.

对于指定地址读的时序, 则为, 指定设备 (Slave Address), 在指定地址 (Reg Address) 下, 读取从机数据 (Data).

第一个字节发送从机地址 (高 7 位) 和写 (1 位), 在接收到应答后再发送一个字节指定寄存器地址, 应答后, 下一个字节再发送一个起始字节, 7 位地址和读 (1 位), 应答后接收数据.

多次执行最后一部分时序, 可以多读或多写. 此时, 前几个字节给应答, 最后一个字节给非应答.

MPU6050

MPU6050 是一个 6 轴姿态传感器, 可以测量芯片自身 X, Y, Z 轴的加速度, 角速度参数, 通过数据融合, 得到姿态角 (欧拉角).

由于其有 3 轴加速度计 (Accelerometer), 用于测量 X, Y, Z 轴的加速度以及 3 轴陀螺仪传感器 (Gyroscope), 用于测量 X, Y, Z 的角速度, 因此总的是 6 轴.

PWR

PWR, PoWeR Control, 电源控制.

PWR 负责管理 STM32 内部的电源供电部分, 可以实现可编程电压监测器和低功耗模式的功能.

可编程电压监测器 (PVD, Programmable Voltage Monitor), 可以监控 VDD 电源电压, 当 VDD 下降到 PVD 阈值以下或上升到 PVD 阈值之上时, PVD 会触发中断, 用于执行紧急关闭任务.

低功耗模式包括睡眠模式 (Sleep), 停机模式 (Stop) 和待机模式 (Standby), 可在系统空闲时, 降低 STM32 的功耗, 延长设备使用时间.

在设备空闲时, 关闭不使用的硬件, 保留必要的唤醒电路, 比如串口接收数据的中断唤醒, 外部中断唤醒, RTC 闹钟唤醒等, 在设备需要工作时, STM32 能够立刻重新投入工作. (如果只考虑进入低功耗而不考虑唤醒则与断电无异)

PVD 的中断是通过 EXTI 实现的 (因此需要配置外部中断), 因为低功耗模式设计的是只有外部中断可以唤醒停止模式, 其他设备若想唤醒停止模式, 都可以借助 EXTI.

低功耗模式

三种模式的基本信息如:

WFI, Wait For Interrupt, 等待中断.

WFE, Wait For Event, 等待事件.

睡眠模式只是关闭了时钟.

这里的电压调节器其实就是 1.8V 区域的电源.

停机模式主要把运行的高速时钟关闭, CPU 和外设都暂停工作, 但是电压调节器没有关闭. 因此存储器和寄存器数据可以维持原样.

待机模式会关闭电压调节器, 也就是说存储器和寄存器数据会丢失, 但是低速时钟 (LSI, LSE) 不会关闭 (用于维持 RTC, WDG 等的运行)

对于需要设置的寄存器以进入某种特定模式如下:

睡眠模式特点总结

执行完 WFI/WFE 指令后, STM32 进入睡眠模式, 程序暂停运行, 唤醒后程序从暂停的地方继续运行.

SLEEPONEXIT 位决定 STM32 执行完 WFI 或 WFE 后, 是立刻进入睡眠, 还是等 STM32 从最低优先级的中断处理程序中退出时进入睡眠.

在睡眠模式下, 所有的 I/O 引脚都保持它们在运行模式时的状态,.

WFI 指令进入睡眠模式, 可被任意一个 NVIC 响应的中断唤醒.

WFE 指令进入睡眠模式, 可被唤醒事件唤醒.

停机模式特点总结

执行完 WFI/WFE 指令后, STM32 进入睡眠模式, 程序暂停运行, 唤醒后程序从暂停的地方继续运行.

1.8V 供电区域的所有时钟都被停止, PLL, HSI 和 HSE 被禁止, SRAM 和寄存器内容被保留下来.

在停止模式, 所有的 I/O 引脚都保持它们在运行模式时的状态.

当一个中断或唤醒事件导致退出停止模式时, HSI 被选为系统时钟.

当电压调节器处于低功耗模式下, 系统从停止模式退出时, 会有一段额外的启动延时.

WFI 指令进入睡眠模式, 可被任意一个 EXTI 中断唤醒.

WFE 指令进入睡眠模式, 可被任意一个 EXTI 事件唤醒.

待机模式特点总结

执行完 WFI/WFE 指令后, STM32 进入待机模式, 唤醒后程序从头开始运行. (因为数据丢失了)

整个 1.8V 供电区域被断电, PLL, HSI 和 HSE 也被断电, SRAM 和寄存器内容丢失, 只有备份的寄存器和待机电路维持供电.

在待机模式下, 所有的 I/O 引脚变为高阻态 (浮空输入).

WKUP 引脚的上升沿, RTC 闹钟事件的上升沿, NRST 引脚上外部复位, IWDG 复位退出待机模式.

Neovim 简单配置

用 Mason 和 lsp-config 来配置出简单的自动补全体验, 基本上在 Mason 中安装 clangd 即可.

由于 keil 5 添加头文件路径的方式我不是很明白, 这里添加头文件就直接使用相对路径, 这样 clangd 也能找到用于补全提示:

arm-none-eabi-objcopy 系列工具的使用

这里主要用 arm-none-eabi-objcopy 来进行格式转换.

将 axf 文件转换为 bin 文件

1
arm-none-eabi-objcopy -O binary test.axf test.bin

调试

参考

RCC 相关库函数

对于 RCC 模块, 常用函数有:

1
2
3
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);

解释一下函数名和形参名中的名词:

  • “AHB”, Advanced High-performance Bus, 一种用于连接处理器和外设的高性能总线
  • “APB2”, Advanced Peripheral Bus 2, 同样是一种用于连接处理器和外设的高性能总线
  • “APB1”, Advanced Peripheral Bus 1
  • “Periph”, Peripheral, 指处理器或外设
  • “Clock”, 指时钟信号
  • “Cmd”, Command, 表示一种行为

这里的 FunctionalState 是一种枚举类型, 其定义为:

1
typedef enum {DISABLE = 0, ENABLE = !DISABLE} FunctionalState;

GPIO 相关库函数

GPIO (General Purpose Input Output), 通用输入输出接口. 其可以配置 8 种输入输出模式.

输出模式下可用于控制端口输出高低电平.

输入模式下可读取端口的高低电平.

使用 GPIO 前一般有三个步骤:

  1. 使用 RCC 开启 GPIO 的时钟
  2. 使用 GPIO_Init 函数初始化 GPIO
  3. 使用输出或者输入的函数控制 GPIO 口

与初始化相关的常用函数和类型

GPIO_TypeDef 和 GPIO_InitTypeDef 类型

GPIO_TypeDef 类型, 其定义为:

1
2
3
4
5
6
7
8
9
10
typedef struct
{
__IO uint32_t CRL; // Control Register Low
__IO uint32_t CRH; // Control Register High
__IO uint32_t IDR; // Input Data Register
__IO uint32_t ODR; // Output Data Register
__IO uint32_t BSRR; // Bit Set/Reset Register
__IO uint32_t BRR; // Bit Reset Register
__IO uint32_t LCKR; // Lock Register
} GPIO_TypeDef;

这个类型定义了一个 “GPIOx”, 其成员用于寄存器映射.

GPIO_InitTypeDef 类型, 其定义为:

1
2
3
4
5
6
7
8
9
10
11
typedef struct
{
uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define */

GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIOSpeed_TypeDef */

GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIOMode_TypeDef */
}GPIO_InitTypeDef;

需要指定:

  • GPIO_Pin, 启用 GPIOx 的哪些引脚
  • GPIO_Speed, 指定引脚可变化的频率 (应该是 on/off 吧)
  • GPIO_Mode, 指定 GPIOx 引脚的输入输出模式 (一共有 8 种)
    注意这个 structure 是与 GPIOx 无关的.

初始化函数

1
2
3
4
5
void GPIO_DeInit(GPIO_TypeDef* GPIOx);

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);

void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);

同样解释这里的命名:

  • DeInit, Deinitialize, 对一组 “GPIOx” 引脚去初始化
  • AFIO, Alternate Function Input/Output, 用于配置引脚备份

这里主要用到的是 GPIO_Init(), 用一个 GPIO_InitTypeDef 结构体来设置 GPIO_TypeDef 的参数.

GPIO_StructInit 是用一组默认值来设置 GPIO_InitTypeDef, 可以从其定义中看出:

1
2
3
4
5
6
7
void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct)
{
/* Reset GPIO init structure parameters values */
GPIO_InitStruct->GPIO_Pin = GPIO_Pin_All;
GPIO_InitStruct->GPIO_Speed = GPIO_Speed_2MHz;
GPIO_InitStruct->GPIO_Mode = GPIO_Mode_IN_FLOATING;
}

与读写相关的常用函数

1
2
3
4
5
6
7
8
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);

这里主要解释下 GPIO_Write() 函数, 设置 PortVal 为:

  • 0x0001, 表示二进制 0000 0000 0000 0001, 二进制的每一位表示一个引脚, 置 1 表示给引脚通高电平, 置 0 表示给引脚通低电平

外部中断 EXTI

设置 EXTI 一般有 5 步:

  1. 配置 RCC, 开启外设时钟
  2. 配置 GPIO, 选择端口为输入模式
  3. 配置 AFIO, 选择输出到 EXTI 的信号
  4. 配置 EXTI, 选择触发方式以及响应方式
  5. 配置 NVIC, 给中断一个合适的优先级
    之后外部中断信号就能够进入 CPU. 同样可以结合这个结构图来理解:

注意 EXTI 和 NVIC 不需要开启时钟, EXTI 原因未知, 但 NVIC 是内核的外设, 都是不需要开启时钟的. RCC 管理的都是内核外的外设.

AFIO 相关库函数

AFIO 的库函数和 GPIO 在同一个文件中 (stm32f10x_gpio.hstm32f10x_gpio.c 文件), 和 AFIO 相关的常用库函数有:

1
2
3
4
5
6
7
8
void GPIO_AFIODeInit(void);

void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
void GPIO_EventOutputCmd(FunctionalState NewState);
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface);
  • GPIO_AFIODeInit() 用于复位 AFIO 的设置, 调用时, APIO 外设所有的配置就会全部清除.
  • GPIO_PinLockConfig() 用于锁定某个 GPIO 引脚的配置, 即这个引脚的配置无法被更改 (防止意外更改)
  • GPIO_EventOutputConfig()GPIO_EventOutputCmd() 用于配置事件输出功能 (用得不多)
  • GPIO_PinRemapConfig() 用于引脚重映射, 可指定重映射的方式和新的状态
  • GPIO_EXTILineConfig(), 用于配置 AFIO 数据选择器来选择中断引脚 (比较重要, 必须设置)
  • GPIO_ETH_MediaInterfaceConfig() 和以太网有关

引脚选择如:

1
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);

EXTI 相关库函数

与 EXTI 相关的库函数定义在 stm32f10x_exti.hstm32f10x_exti.h 中, 有:

1
2
3
4
5
6
7
8
void EXTI_DeInit(void);
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct);
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);
void EXTI_ClearFlag(uint32_t EXTI_Line);
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
  • EXTI_DeInit(), 可以将 EXTI 的配置清除, 恢复成上电默认的状态
  • EXTI_Init(), 根据 EXTI_InitTypeDef 类型结构体中的参数来配置 EXTI 外设
  • EXTI_StructInit(), 给一个结构体赋默认值
  • EXTI_GenerateSWInterrupt(), 用于软件中断

四个查看状态寄存器的函数:

  • EXTI_GetFlagStatus(), 查看指定的标志位是否被置 1
  • EXTI_ClearFlag() 对置 1 的标志位进行清除
  • EXTI_GetITStatus() 获取中断标志位是否置 1
  • EXTI_ClearITPendingBit(), 清除中断标志位
    建议在主程序中用上两个, 在中断程序中用下面两个.

EXTI 初始化相关结构体

EXTI_InitTypeDef, 其定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct {
uint32_t EXTI_Line; /*!< Specifies the EXTI lines to be enabled or disabled.
This parameter can be any combination of @ref EXTI_Lines */

EXTIMode_TypeDef EXTI_Mode; /*!< Specifies the mode for the EXTI lines.
This parameter can be a value of @ref EXTIMode_TypeDef */

EXTITrigger_TypeDef EXTI_Trigger; /*!< Specifies the trigger signal active edge for the EXTI lines.
This parameter can be a value of @ref EXTIMode_TypeDef */

FunctionalState EXTI_LineCmd; /*!< Specifies the new state of the selected EXTI lines.
This parameter can be set either to ENABLE or DISABLE */
} EXTI_InitTypeDef;

创建和初始化如:

1
2
3
4
5
6
EXTI_InitTypeDef EXTI_InitStructure;

EXTI_InitStructure.EXTI_Line = EXTI_Line14;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;

NVIC 相关库函数

NVIC 的相关库函数在 misc.hmisc.c 中. 有:

1
2
3
4
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);
void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState);
  • NVIC_PriorityGroupConfig() 用于 NVIC 分组, 就是指用几位来抢占, 几位来响应. (注意分组对于一个芯片只设计一次, 也就是一种)
  • NVIC_Init(), 根据 NVIC_InitTypeDef 类型结构体内容初始化 NVIC
  • NVIC_SetVectorTable() 用于设置中断向量表 (用得不多)
  • NVIC_SystemLPConfig() 用于系统低功耗配置 (用得不多)

“pre-emption priority” (先占) 指抢占优先级, “subpriority” (从占) 指响应优先级.

NVIC 初始化相关结构体

NVIC_InitTypeDef, 其定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct {
uint8_t NVIC_IRQChannel; /*!< Specifies the IRQ channel to be enabled or disabled.
This parameter can be a value of @ref IRQn_Type
(For the complete STM32 Devices IRQ Channels list, please
refer to stm32f10x.h file) */

uint8_t NVIC_IRQChannelPreemptionPriority; /*!< Specifies the pre-emption priority for the IRQ channel
specified in NVIC_IRQChannel. This parameter can be a value
between 0 and 15 as described in the table @ref NVIC_Priority_Table */

uint8_t NVIC_IRQChannelSubPriority; /*!< Specifies the subpriority level for the IRQ channel specified
in NVIC_IRQChannel. This parameter can be a value
between 0 and 15 as described in the table @ref NVIC_Priority_Table */

FunctionalState NVIC_IRQChannelCmd; /*!< Specifies whether the IRQ channel defined in NVIC_IRQChannel
will be enabled or disabled.
This parameter can be set either to ENABLE or DISABLE */
} NVIC_InitTypeDef;

创建和初始化如:

1
2
3
4
5
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = ; // 指定中断通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ; // 指定中断通道是 enable 还是 disable
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = ; // 指定 pre-emption priority
NVIC_InitStructure.NVIC_IRQChannelSubPriority = ; // 指定 subpriority

中断函数编写

在 STM32 中, 中断函数的名称都是固定的, 每个中断通道都对应一个中断函数. 名字可以参考启动文件, 如这里的 startup_stm32f10x_md.s, 以 IRQHandler (Interrupt Request Handler) 结尾的就是中断函数的名称. 如:

中断函数都是没有参数和返回值的, 示例如:

1
2
3
4
5
6
void EXTI15_10_IRQHandler(void) {
if (EXTI_GetITStatus(EXTI_Line14) == SET) {
CountSensor_Count++;
EXTI_ClearITPendingBit(EXTI_Line14);
}
}

一般先进行中断标志位的判断, 确保是想要的中断源触发的函数.

中断函数中执行完主体工作后, 记得调用将中断标志位清除的函数. 如果不清除标志位, 则会一直申请中断, 导致程序不断响应中断, 执行中断函数, 从而卡死.

注意中断函数不需要在 header 文件中声明, 因为其不需要被手动调用, 而是自动执行.

中断函数中建议不执行过长的代码, 要简短快速, 因为中断是处理突发的事情, 如果处理中断时间过长, 主程序就会受到严重阻塞.

最好不要在中断函数和主函数中调用相同的函数或者操作同一个硬件. 对于 CPU, 在进入中断时会有现场保护操作, 但是对于外设硬件, 就没有, 此时中断返回后容易出错.

TIM 定时中断

配置 TIM 定时中断的大体步骤为:

  1. RCC 开启时钟, 此时定时器的基准时钟和整个外设的工作时钟就都会同时打开
  2. 选择时基单元的时钟源, 对于定时中断, 一般选用内部时钟源
  3. 配置时基单元, 包括预分频器, 自动重装器, 计数器
  4. 配置中断输出控制, 允许更新中断输出到 NVIC
  5. 配置 NVIC, 在 NVIC 中打开定时器中断的通道, 并分配一个优先级
  6. 配置运行控制, 使能下计数器, 之后计数及就会开始计数, 当计数器更新时, 触发中断
  7. 编写中断函数

定时器相关库函数

位于 stm32f10x_tim.hstm32f10x_tim.c 文件中, 常用的有:

1
2
3
4
5
void TIM_DeInit(TIM_TypeDef* TIMx);
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
  • TIM_DeInit(), 用于恢复缺省配置
  • TIM_TimeBaseInit(), 配置时基单元
  • TIM_TimeBaseStructInit(), 给 TIM_TimeBaseInitTypeDef 类型结构体赋默认值
  • TIM_Cmd(), 使能计数器
  • TIM_ITConfig(), 使能中断输出信号

与时基单元时钟选择部分相关的六个函数:

1
2
3
4
5
6
7
8
9
10
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,
uint16_t TIM_ICPolarity, uint16_t ICFilter);
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
  • TIM_InternalClockConfig(), 选择内部时钟
  • TIM_ITRxExternalClockConfig(), 选择 ITRx 其他定时器的时钟
  • TIM_TIxExternalClockConfig(), 选择 TIx 捕获通道的时钟
  • TIM_ETRClockMode1Config(), 选择 ETR 引脚通过外部时钟模式 1 输入的时钟
  • TIM_ETRClockMode2Config(), 选择 ETR 引脚通过外部时钟模式 2 输入的时钟
  • TIM_ETRConfig(), 单独用于配置 ETR 引脚的预分频器, 极性, 滤波器这些参数

用于在初始化之后单独更改一些配置的函数:

1
2
3
4
5
6
7
8
9
10
11
12
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);

void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);

void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);

void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);

uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);

uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
  • TIM_PrescalerConfig(), 配置预分频值
  • TIM_CounterModeConfig(), 改变计数器的计数模式
  • TIM_ARRPreloadConfig(), 配置是否启用自动重装器预装功能
  • TIM_SetCounter(), 设置计数器的值
  • TIM_SetAutoreload(), 设置自动重装器的值
  • TIM_GetCounter(), 获取当前计数器的值
  • TIM_GetPrescaler(), 获取当前预分频器的值

与获取标志位和清除标志位相关的函数:

1
2
3
4
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);

相关结构体

TIM_TypeDef 的定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
typedef struct
{
__IO uint16_t CR1;
uint16_t RESERVED0;
__IO uint16_t CR2;
uint16_t RESERVED1;
__IO uint16_t SMCR;
uint16_t RESERVED2;
__IO uint16_t DIER;
uint16_t RESERVED3;
__IO uint16_t SR;
uint16_t RESERVED4;
__IO uint16_t EGR;
uint16_t RESERVED5;
__IO uint16_t CCMR1;
uint16_t RESERVED6;
__IO uint16_t CCMR2;
uint16_t RESERVED7;
__IO uint16_t CCER;
uint16_t RESERVED8;
__IO uint16_t CNT;
uint16_t RESERVED9;
__IO uint16_t PSC;
uint16_t RESERVED10;
__IO uint16_t ARR;
uint16_t RESERVED11;
__IO uint16_t RCR;
uint16_t RESERVED12;
__IO uint16_t CCR1;
uint16_t RESERVED13;
__IO uint16_t CCR2;
uint16_t RESERVED14;
__IO uint16_t CCR3;
uint16_t RESERVED15;
__IO uint16_t CCR4;
uint16_t RESERVED16;
__IO uint16_t BDTR;
uint16_t RESERVED17;
__IO uint16_t DCR;
uint16_t RESERVED18;
__IO uint16_t DMAR;
uint16_t RESERVED19;
} TIM_TypeDef;

就是指 TIM1, TIM2 这些.

TIM 输出比较

PWM 驱动基础

基本步骤:

  1. RCC 开启 TIM 外设和 GPIO 外设的时钟
  2. 配置时基单元, 包括时钟源选择
  3. 配置输出比较单元, 包括 CRR 的值, 输出比较模式, 极性选择, 输出使能等
  4. 配置 GPIO, 怕 PWM 对应的 GPIO 口, 初始化为复用推挽输出的配置
  5. 配置运行控制, 启用计数器

和 OC 相关的 TIM 库函数

1
2
3
4
5
void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC4Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
  • TIM_OC 就是指 “TIMer Output Compare”, 由于有 4 个 OC 单元, 因此这里有四个函数分别配置
  • TIM_OCStructInit()TIM_OCInitTypeDef 类型结构体赋初始值

单独更改 CCR 寄存器值的函数:

1
2
3
4
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);
void TIM_SetCompare3(TIM_TypeDef* TIMx, uint16_t Compare3);
void TIM_SetCompare4(TIM_TypeDef* TIMx, uint16_t Compare4);

可以在运行时更改占空比.

重映射时需要参考手册中的重映射表. 若重映射到调试端口, 记得解除调试映射.

当同时初始化同一个定时器的不同通道的 Output Compare 时, 由于他们共用一个计数器, 因此频率需要一致, 但占空比可以各自设定 (即设置 CCR 的值). 由于在计数器更新时, 所有 PWM 同时跳变, 因此它们的相位都一致.

和 OC 相关结构体

TIM_OCInitTypeDef

TIM 输入捕获

一般步骤为:

  1. RCC 开启 GPIO 和 TIM 的时钟
  2. GPIO 初始化, 把 GPIO 配置成输入模式, 这里一般为上拉输入或者浮空输入模式
  3. 配置时基单元, 让 CNT 计数器在内部时钟的驱动下自增运行
  4. 配置输入捕获单元, 包括滤波器, 极性, 直连通道, 交叉通道, 分频器等参数
  5. 选择从模式触发源
  6. 选择触发之后的操作
  7. 当电路都配置好之后, 调用 TIM_Cmd 函数, 开启定时器

当到读取最新一个周期的频率时, 直接读取 CCR 寄存器, 然后按照 $fc/N$ 来计算.

相关库函数

位于 stm32f10x_tim.cstm32f10x_tim.h 文件中:

1
2
3
4
5
6
void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
void TIM_ICStructInit(TIM_ICInitTypeDef* TIM_ICInitStruct);
void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
  • TIM_ICInit(), 用 TIM_ICInitTypeDef 类型结构体来初始化 TIM_TypeDef 也就是具体的定时器, 这里需要指定具体配置哪一个通道
  • TIM_PWMIConfig(), 可以把定时器设置为 PWMI 模式, 并同时初始化两个通道
  • TIM_ICStructInit(), 可以给 TIM_ICInitTypeDef 类型结构体赋一个初始值
  • TIM_SelectInputTrigger(), 选择输入触发源 TRGI
  • TIM_SelectOutputTrigger(), 选择输出触发源 TRGO
  • TIM_SelectSlaveMode(), 选择从模式

分别读取四个通道的 CCR 寄存器的函数为:

1
2
3
4
uint16_t TIM_GetCapture1(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture2(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture3(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture4(TIM_TypeDef* TIMx);

TIM_PWMIConfig() 只支持 channel1 和 channel2 的配置, 不能传给 channel3 和 channel4. 当配置 channel1 后, 其会自动把 channel2 配置为相反的参数.

正负1误差可以认为是 1/计数值. 比如: 要求误差等于千分之一, 则频率上限为 $1M/1000 = 1kHz$. 要求误差等于百分之一, 则频率上限为 $1M/100 = 10kHz$

TIM 编码器接口

编码器接口的基本结构为:

驱动编写的基本步骤为:

  1. RCC 开启时钟, 开启 GPIO 和定时器的时钟
  2. 配置 GPIO, 这里把 PA6 和 PA7 配置成输入模式
  3. 配置时基单元, 这里的预分频器就设置为不分频, 自动重装设最大 65535
  4. 配置输入捕获单元
  5. 配置编码器接口模式
  6. 调用 TIM_Cmd() 启动定时器

这里不需要配置定时器内部时钟, 因为编码器接口会托管时钟, 也就是说, 其为一个带方向控制的外部时钟.

相关库函数

这里列出之前没列出的函数:

1
2
void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx, uint16_t TIM_EncoderMode,
uint16_t TIM_IC1Polarity, uint16_t TIM_IC2Polarity);

如:

1
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);

若想改变旋转的极性, 修改一个即可:

1
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Falling, TIM_ICPolarity_Rising);

需要注意的事

按键抖动和松手检测

对于外部中断, 不容易处理按键抖动和松手检测.

编码器输出正交波形

预分频器的实际分频系数

1
实际分频系数 = 预分频器值 + 1

如, 设置预分频器值为 1, 假设输入频率为 72MHz, 则变为 72MHz/2. 如果设置值为 2, 则频率为 72MHz/3.

消除 Motor 里的电流声

当 PWM 的频率超过人耳听到声音的频率范围, 即 $20Hz ~ 20kHz$, 即可.

1
2
3
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;		//ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 36 - 1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;

Linux-下-STM32f103c8-开发板开发技巧
http://example.com/2024/03/25/Linux-下-STM32f103c8-开发板开发技巧/
作者
Jie
发布于
2024年3月25日
许可协议