В
今天,我将向您展示如何使用数据表来解决 STM32(Blue Pill)和 STM8 控制器上的许多项目、任务,非常简单,但必需。 所有演示项目都专门针对我最喜欢的 LED,我们将大量点亮它们,为此我们将不得不使用各种有趣的外围设备。
文本再次变得很大,所以为了方便起见,我制作了内容:
免责声明:我不是工程师,我不会假装对电子学有很深的了解,这篇文章是为像我这样的业余爱好者而写的。 事实上,两年前我就将自己视为目标受众。 如果当时有人告诉我,陌生芯片上的数据表读起来并不可怕,我就不会花很多时间在互联网上寻找一些代码,并发明剪刀和胶带拐杖。
本文的重点是数据表,而不是项目,因此代码可能不是很整洁并且常常很局促。 这些项目本身非常简单,但适合初次接触新芯片。
我希望我的文章能够帮助处于类似阶段的沉浸在这个爱好中的人。
STM32
16 个 LED,带 DM634 和 SPI
使用 Blue Pill (STM32F103C8T6) 和 DM634 LED 驱动器的小型项目。 使用数据表,我们将找出驱动程序、STM IO 端口并配置 SPI。
DM634
台湾芯片,16路16位PWM输出,可串接。 低端12位型号是从国内一个项目得知的
由于制造商是台湾人,
直流灌源(漏极开路)
水槽 / 开漏输出 - 流走; 流入电流源; 输出在活动状态下接地 - LED 通过阴极连接到驱动器。 从电气角度来说,这当然不是“漏极开路”(漏极开路),但在数据表中经常会发现漏极模式引脚的这种指定。
REXT 和 GND 之间的外部电阻用于设置输出电流值
REXT 引脚和地之间安装了一个参考电阻,用于控制输出的内阻,请参见数据表第 9 页上的图表。 在DM634中,这个电阻也可以通过软件控制,设置整体亮度(全局亮度); 我不会在本文中详细介绍,我只是在这里放置一个 2.2 - 3 kOhm 的电阻。
为了了解如何控制芯片,我们看一下设备接口的说明:
是的,这就是中文英语的全部辉煌。 翻译这个是有问题的,如果你愿意的话可以理解它,但是还有另一种方法——看看数据表中如何描述与功能类似的TLC5940的连接:
...只需三个引脚即可将数据输入设备。 SCLK 信号的上升沿将数据从 SIN 引脚移至内部寄存器。 所有数据加载完毕后,一个短的高 XLAT 信号将顺序传输的数据锁存到内部寄存器中。 内部寄存器是由 XLAT 信号电平触发的门。 所有数据首先传输最高有效位。
锁存器 – 闩锁/闩锁/锁。
上升沿 – 脉冲前沿
MSB优先 – 最重要(最左边)位向前。
时钟数据 – 顺序传输数据(逐位)。
字 闩 经常在芯片的文档中找到,并以各种方式翻译,因此为了理解,我将允许自己
小型教育计划LED 驱动器本质上是一个移位寄存器。 “转移” (转移)在名称中 - 设备内部数据的按位移动:推入内部的每个新位都会将整个链向前推动。 由于没有人愿意在换档期间观察 LED 的混乱闪烁,因此该过程发生在通过阻尼器与工作寄存器分开的缓冲寄存器中(闩)是一种等待室,其中的位按所需的顺序排列。 当一切准备就绪后,快门打开,零件开始工作,替换之前的批次。 单词 闩 在微电路的文档中几乎总是暗示这样的阻尼器,无论它以何种组合使用。
因此,向 DM634 的数据传输是这样进行的:将 DAI 输入设置为远端 LED 的最高有效位的值,拉高和拉低 DCK; 将DAI输入设置为下一位的值,拉动DCK; 依此类推,直到所有位均已传输(计时),之后我们拉 LAT。 这可以手动完成(位爆炸),但最好使用为此专门定制的 SPI 接口,因为它在我们的 STM32 上有两个副本。
蓝丸STM32F103
简介:STM32 控制器比 Atmega328 复杂得多,比它们看起来可怕的多。 此外,出于节能的原因,几乎所有外设在启动时都被关闭,并且时钟频率为来自内部源的8 MHz。 幸运的是,STM 程序员编写了代码,使芯片达到了“计算出的”72 MHz,而且我所知道的所有 IDE 的作者都将其包含在初始化过程中,因此我们不需要时钟(但是
文档:Blue Pill配备了流行的STM32F103C8T6芯片,有两个有用的文档:
在数据表中,我们可能感兴趣:
- 引脚排列 – 芯片引脚排列 – 如果我们决定自己制作电路板;
- 内存映射——特定芯片的内存映射。 参考手册有整条线的地图,它提到了我们没有的寄存器。
- 引脚定义表——列出引脚的主要功能和替代功能; 对于“蓝色药丸”,您可以在互联网上找到更方便的图片,其中包含引脚及其功能列表。 因此,我们立即谷歌搜索 Blue Pill 引脚排列,并将这张图片放在手边:
注:图片来自网络,有错误,已在评论中指出,谢谢。 图片已被替换,但这是一个教训 - 最好不要从数据表中检查信息。
我们删除数据表,打开参考手册,从现在开始我们只使用它。
流程:我们处理标准输入/输出,配置SPI,打开必要的外设。
输入输出
在 Atmega328 上,I/O 的实现极其简单,这就是为什么丰富的 STM32 选项可能会令人困惑。 现在我们只需要结论,但即使是这些也有四种选择:
开漏、推挽、交替推挽、交替开漏
“拉推”(推拉)是 Arduino 的通常输出,该引脚可以采用高电平或低电平值。 但对于“漏极开路”有
输出配置 / 当端口分配为输出时: / 输出缓冲器启用: / – 开漏模式:输出寄存器中的“0”启用 N-MOS,输出寄存器中的“1”使端口处于 Hi-Z 模式( P-MOS 未激活 ) / – 推挽模式:输出寄存器中的“0”激活 N-MOS,输出寄存器中的“1”激活 P-MOS。
开漏极之间的所有区别(漏极开路)来自“推拉”(推拉)的问题是第一个引脚不能接受高电平状态:当向输出寄存器写入一个时,它会进入高阻模式(高阻抗, 高阻抗)。 当写入零时,引脚在两种模式下的行为相同,无论是逻辑上还是电气上。
在正常输出模式下,引脚仅广播输出寄存器的内容。 在“替代”中,它由相应的外设控制(见9.1.4):
如果端口位被配置为复用功能引脚,则引脚寄存器将被禁用,并且该引脚将连接到外设引脚。
每个引脚的替代功能描述于 引脚定义 数据表位于下载的图像上。 对于如果一个引脚有多个替代功能该怎么办的问题,数据表中的脚注给出了答案:
如果多个外设使用同一引脚,为了避免替代功能之间的冲突,一次只能使用一个外设,并使用外设时钟使能位(在相应的 RCC 寄存器中)进行切换。
最后,输出模式下的引脚也有时钟速度。 这是另一个节能功能;在我们的例子中,我们只是将其设置为最大值然后忘记它。
所以:我们使用SPI,这意味着两个引脚(带有数据和带有时钟信号)应该是“替代推挽功能”,另一个(LAT)应该是“常规推挽”。 但在分配它们之前,让我们先处理一下 SPI。
SPI
另一个小型教育项目
SPI 或 Serial Peripheral Interface(串行外设接口)是一种简单且非常有效的接口,用于连接 MK 与其他 MK 以及外部世界。 其工作原理已经在上面介绍过中文LED驱动器(在参考手册中,参见第25节)。 SPI 可以在主机(“master”)和从机(“slave”)模式下运行。 SPI 有四个基本通道,其中并非全部都可以使用:
- MOSI,主机输出/从机输入:该引脚在主机模式下发送数据,在从机模式下接收数据;
- MISO,Master Input/Slave Output:相反,在主机中接收,在从机中发送;
- SCK,串行时钟:设置主机中数据传输的频率或从机中接收时钟信号。 本质上是敲击节拍;
- SS,奴隶选择:在这个通道的帮助下,奴隶知道有人想要他的东西。 在 STM32 上称为 NSS,其中 N = 负数,即如果该通道接地,控制器将成为从机。 它与开漏输出模式结合得很好,但那是另一个故事了。
与其他事物一样,STM32 上的 SPI 功能丰富,这使得它有些难以理解。 例如,它不仅可以与SPI配合使用,还可以与I2S接口配合使用,而在文档中它们的描述是混杂的,需要及时剪掉多余的部分。 我们的任务非常简单:我们只需要使用 MOSI 和 SCK 发送数据。 我们转到第25.3.4节(半双工通信,半双工通信),在那里我们发现 1个时钟和1个单向数据线 (1个时钟信号和1个单向数据流):
在此模式下,应用程序在仅发送或仅接收模式下使用 SPI。 / 仅发送模式与双工模式类似:数据在发送引脚(主模式下为 MOSI 或从模式下为 MISO)上传输,而接收引脚(分别为 MISO 或 MOSI)可用作常规 I/O 引脚。 在这种情况下,应用程序只需忽略 Rx 缓冲区(如果读取该缓冲区,则那里不会有传输的数据)。
太好了,MISO 引脚是空闲的,让我们将 LAT 信号连接到它。 我们看一下Slave Select,在STM32上可以通过编程来控制,极其方便。 我们在第 25.3.1 SPI 概述中读到同名段落:
软件控制 NSS(SSM = 1)/从机选择信息包含在 SPI_CR1 寄存器的 SSI 位中。 外部 NSS 引脚仍可用于其他应用需求。
是时候写入寄存器了。 我决定使用 SPI2,在数据表中查找其基址 - 在第 3.3 节内存映射中:
好吧,让我们开始吧:
#define _SPI2_(mem_offset) (*(volatile uint32_t *)(0x40003800 + (mem_offset)))
打开第 25.3.3 节,其标题不言自明“在主模式下配置 SPI”:
1. 使用 SPI_CR2 寄存器中的 BR[0:1] 位设置串行时钟频率。
这些寄存器收集在同名的参考手册部分中。 地址移位(地址偏移量)对于 CR1 – 0x00,默认情况下所有位都被清除(重置值 0x0000):
BR 位设置控制器时钟分频器,从而确定 SPI 的运行频率。 我们的 STM32 频率为 72 MHz,根据其数据表,LED 驱动器的工作频率高达 25 MHz,因此我们需要除以四 (BR[2:0] = 001)。
#define _SPI_CR1 0x00
#define BR_0 0x0008
#define BR_1 0x0010
#define BR_2 0x0020
_SPI2_ (_SPI_CR1) |= BR_0;// pclk/4
2. 设置 CPOL 和 CPHA 位以定义数据传输和串行时钟时序之间的关系(参见第 240 页上的图表)
由于我们在这里阅读的是数据表而不是原理图,因此让我们仔细看看第 704 页上的 CPOL 和 CPHA 位的文本描述(SPI 概述):
时钟相位和极性
使用 SPI_CR1 寄存器的 CPOL 和 CPHA 位,您可以通过编程方式选择四种时序关系。 当没有数据传输时,CPOL(时钟极性)位控制时钟信号的状态。 该位控制主从模式。 如果 CPOL 复位,则 SCK 引脚在休眠模式下为低电平。 如果 CPOL 位被置位,则 SCK 引脚在休息模式期间为高电平。
当 CPHA(时钟相位)位置位时,高位陷阱选通是 SCK 信号的第二个边沿(如果 CPOL 清零则下降,如果 CPOL 置位则上升)。 数据由时钟信号的第二次变化捕获。 如果 CPHA 位清零,则高位陷阱选通是 SCK 信号的上升沿(如果 CPOL 置 XNUMX,则为下降沿;如果 CPOL 被清零,则为上升沿)。 数据在时钟信号第一次变化时被捕获。
吸收了这些知识后,我们得出结论:这两个位必须保持为零,因为我们希望 SCK 信号在不使用时保持低电平,并在脉冲的上升沿传输数据(见图 XNUMX)。 上升沿 在 DM634 数据表中)。
顺便说一下,在这里我们第一次遇到了ST数据表中词汇的一个特点:其中写着“将位重置为零”这句话 重置一点而且不 清除一点,例如 Atmega。
3、设置DFF位,判断数据块是8位还是16位格式
我特意选择了16位DM634,以免像DM12那样费心传输633位PWM数据。 将 DFF 设置为 XNUMX 是有意义的:
#define DFF 0x0800
_SPI2_ (_SPI_CR1) |= DFF; // 16-bit mode
4. 配置SPI_CR1寄存器中的LSBFIRST位来确定块格式
LSBFIRST,顾名思义,配置传输时首先使用最低有效位。 但DM634希望从最高有效位开始接收数据。 因此,我们将其重置。
5. 在硬件模式下,如果需要从 NSS 引脚输入,请在整个字节传输序列期间向 NSS 引脚施加高电平信号。 在 NSS 软件模式下,设置 SPI_CR1 寄存器中的 SSM 和 SSI 位。 如果 NSS 引脚用作输出,则只需设置 SSOE 位即可。
安装SSM和SSI以忘记NSS硬件模式:
#define SSI 0x0100
#define SSM 0x0200
_SPI2_ (_SPI_CR1) |= SSM | SSI; //enable software control of SS, SS high
6. 必须设置 MSTR 和 SPE 位(仅当 NSS 信号为高电平时它们才保持设置状态)
实际上,通过这些位,我们将 SPI 指定为主机并将其打开:
#define MSTR 0x0004
#define SPE 0x0040
_SPI2_ (_SPI_CR1) |= MSTR; //SPI master
//когда все готово, включаем SPI
_SPI2_ (_SPI_CR1) |= SPE;
SPI 配置完毕,让我们立即编写向驱动程序发送字节的函数。 继续阅读25.3.3“在主模式下配置SPI”:
数据传输顺序
当一个字节写入 Tx 缓冲区时,传输开始。
数据字节被加载到移位寄存器中 平行 模式(来自内部总线)在第一位传输期间,之后它被传输到 顺序的 MOSI 引脚模式,第一位或最后一位向前传送取决于 CPI_CR1 寄存器中 LSBFIRST 位的设置。 数据发送后设置TXE标志 从 Tx 缓冲区到移位寄存器,并且如果 CPI_CR1 寄存器中的 TXEIE 位被置位,也会生成中断。
我在翻译中突出显示了几个单词,以引起人们对 STM 控制器中 SPI 实现的一项功能的关注。 在 Atmega 上,TXE 标志(发送空,Tx 为空并准备好接收数据)仅在发送整个字节后才设置 出。 这里,该标志是在字节插入内部移位寄存器后设置的。 由于它是与所有位同时(并行)推送到那里,然后顺序发送数据,因此在字节完全发送之前,TXE 被置位。 这很重要,因为对于我们的 LED 驱动器,我们需要在发送后拉动 LAT 引脚 所有 数据,即仅 TXE 标志对我们来说还不够。
这意味着我们需要另一个标志。 让我们看一下 25.3.7 - “状态标志”:
<...>
忙碌标志
BSY 标志由硬件设置和清除(对其写入无效)。 BSY 标志指示 SPI 通信层的状态。
它重置:
当传输完成时(除非在主模式下,如果传输是连续的)
当 SPI 被禁用时
当发生主模式错误时(MODF=1)
如果传输不连续,则 BSY 标志在每次数据传输之间被清除
好的,这会派上用场的。 让我们找出 Tx 缓冲区所在的位置。 为此,请阅读“SPI 数据寄存器”:
位 15:0 DR[15:0] 数据寄存器
接收到的数据或要发送的数据。
数据寄存器分为两个缓冲区 - 一个用于写入(发送缓冲区),一个用于读取(接收缓冲区)。 写入数据寄存器将写入 Tx 缓冲区,从数据寄存器读取将返回 Rx 缓冲区中包含的值。
好吧,还有状态寄存器,其中有 TXE 和 BSY 标志:
我们写:
#define _SPI_DR 0x0C
#define _SPI_SR 0x08
#define BSY 0x0080
#define TXE 0x0002
void dm_shift16(uint16_t value)
{
_SPI2_(_SPI_DR) = value; //send 2 bytes
while (!(_SPI2_(_SPI_SR) & TXE)); //wait until they're sent
}
好吧,由于我们需要传输两个字节的 16 倍,根据 LED 驱动器输出的数量,如下所示:
void sendLEDdata()
{
LAT_low();
uint8_t k = 16;
do
{ k--;
dm_shift16(leds[k]);
} while (k);
while (_SPI2_(_SPI_SR) & BSY); // finish transmission
LAT_pulse();
}
但我们还不知道如何拉动 LAT 引脚,所以我们将回到 I/O。
分配引脚
在STM32F1中,负责引脚状态的寄存器非常不寻常。 很明显,它们的数量比Atmega要多,但它们也与其他STM芯片不同。 9.1节GPIO概述:
每个通用 I/O 端口 (通用输入接口) 具有两个32位配置寄存器(GPIOx_CRL和GPIOx_CRH)、两个32位数据寄存器(GPIOx_IDR和GPIOx_ODR)、一个32位设置/复位寄存器(GPIOx_BSRR)、一个16位复位寄存器(GPIOx_BRR)和一个32位复位寄存器(GPIOx_BRR)。位阻塞寄存器(GPIOx_LCKR)。
前两个寄存器很不寻常,而且也很不方便,因为 16 个端口引脚以“每个兄弟四位”的格式分散在它们之间。 那些。 引脚 XNUMX 到 XNUMX 位于 CRL,其余引脚位于 CRH。 同时,其余寄存器成功包含端口所有引脚的位 - 通常保留一半“保留”。
为了简单起见,让我们从列表的末尾开始。
我们不需要阻塞寄存器。
设置和重置寄存器非常有趣,因为它们部分地相互重复:您只能在 BSRR 中写入所有内容,其中高 16 位将引脚重置为零,低位将设置为 1,或者您也可以使用BRR,其低16位仅复位引脚。 我喜欢第二个选择。 这些寄存器很重要,因为它们提供对引脚的原子访问:
原子设置或重置
在位级别编程 GPIOx_ODR 时无需禁用中断:可以通过单个原子写入操作 APB2 更改一个或多个位。 这是通过向需要更改的位的置位/复位寄存器(GPIOx_BSRR 或仅用于复位的 GPIOx_BRR)写入“1”来实现的。 其他位将保持不变。
数据寄存器的名称非常不言自明 - IDR = 输入 方向寄存器,输入寄存器; ODR = 输出 方向寄存器,输出寄存器。 我们在当前项目中不需要它们。
最后,控制寄存器。 由于我们对第二个 SPI 引脚(即 PB13、PB14 和 PB15)感兴趣,因此我们立即查看 CRH:
我们发现我们需要在 20 到 31 位之间写入一些内容。
上面我们已经弄清楚了我们想要从引脚中得到什么,所以这里我将不截图,我只是说 MODE 指定方向(如果两个位都设置为 0,则输入)和引脚速度(我们需要 50MHz,即两个引脚都为“1”),CNF 设置模式:常规“推挽”- 00,“替代”- 10。默认情况下,如上所示,所有引脚都有倒数第三位(CNF0),它将它们设置为模式 浮动输入.
由于我计划用这个芯片做其他事情,为了简单起见,我为下控制寄存器和上控制寄存器定义了所有可能的 MODE 和 CNF 值。
好吧,就像那样
#define CNF0_0 0x00000004
#define CNF0_1 0x00000008
#define CNF1_0 0x00000040
#define CNF1_1 0x00000080
#define CNF2_0 0x00000400
#define CNF2_1 0x00000800
#define CNF3_0 0x00004000
#define CNF3_1 0x00008000
#define CNF4_0 0x00040000
#define CNF4_1 0x00080000
#define CNF5_0 0x00400000
#define CNF5_1 0x00800000
#define CNF6_0 0x04000000
#define CNF6_1 0x08000000
#define CNF7_0 0x40000000
#define CNF7_1 0x80000000
#define CNF8_0 0x00000004
#define CNF8_1 0x00000008
#define CNF9_0 0x00000040
#define CNF9_1 0x00000080
#define CNF10_0 0x00000400
#define CNF10_1 0x00000800
#define CNF11_0 0x00004000
#define CNF11_1 0x00008000
#define CNF12_0 0x00040000
#define CNF12_1 0x00080000
#define CNF13_0 0x00400000
#define CNF13_1 0x00800000
#define CNF14_0 0x04000000
#define CNF14_1 0x08000000
#define CNF15_0 0x40000000
#define CNF15_1 0x80000000
#define MODE0_0 0x00000001
#define MODE0_1 0x00000002
#define MODE1_0 0x00000010
#define MODE1_1 0x00000020
#define MODE2_0 0x00000100
#define MODE2_1 0x00000200
#define MODE3_0 0x00001000
#define MODE3_1 0x00002000
#define MODE4_0 0x00010000
#define MODE4_1 0x00020000
#define MODE5_0 0x00100000
#define MODE5_1 0x00200000
#define MODE6_0 0x01000000
#define MODE6_1 0x02000000
#define MODE7_0 0x10000000
#define MODE7_1 0x20000000
#define MODE8_0 0x00000001
#define MODE8_1 0x00000002
#define MODE9_0 0x00000010
#define MODE9_1 0x00000020
#define MODE10_0 0x00000100
#define MODE10_1 0x00000200
#define MODE11_0 0x00001000
#define MODE11_1 0x00002000
#define MODE12_0 0x00010000
#define MODE12_1 0x00020000
#define MODE13_0 0x00100000
#define MODE13_1 0x00200000
#define MODE14_0 0x01000000
#define MODE14_1 0x02000000
#define MODE15_0 0x10000000
#define MODE15_1 0x20000000
我们的引脚位于端口 B(基地址 – 0x40010C00),代码:
#define _PORTB_(mem_offset) (*(volatile uint32_t *)(0x40010C00 + (mem_offset)))
#define _BRR 0x14
#define _BSRR 0x10
#define _CRL 0x00
#define _CRH 0x04
//используем стандартный SPI2: MOSI на B15, CLK на B13
//LAT пусть будет на неиспользуемом MISO – B14
//очищаем дефолтный бит, он нам точно не нужен
_PORTB_ (_CRH) &= ~(CNF15_0 | CNF14_0 | CNF13_0 | CNF12_0);
//альтернативные функции для MOSI и SCK
_PORTB_ (_CRH) |= CNF15_1 | CNF13_1;
//50 МГц, MODE = 11
_PORTB_ (_CRH) |= MODE15_1 | MODE15_0 | MODE14_1 | MODE14_0 | MODE13_1 | MODE13_0;
并且,相应地,您可以编写 LAT 的定义,该定义将由 BRR 和 BSRR 寄存器调整:
/*** LAT pulse – high, then low */
#define LAT_pulse() _PORTB_(_BSRR) = (1<<14); _PORTB_(_BRR) = (1<<14)
#define LAT_low() _PORTB_(_BRR) = (1<<14)
(LAT_low只是惯性,一直都是这样,就这样吧)
现在一切都很好,但行不通。 因为这是 STM32,所以它们可以节省电力,这意味着您需要启用所需外设的时钟。
打开时钟
手表又称时钟,负责计时。 我们已经注意到缩写 RCC。 我们在文档中查找:这是复位和时钟控制。
如上所述,幸运的是,时钟主题中最困难的部分是由 STM 的人员为我们完成的,对此我们非常感谢他们(我将再次给出链接
#define _RCC_(mem_offset) (*(volatile uint32_t *)(0x40021000 + (mem_offset)))
然后单击您尝试在板中查找某些内容的链接,或者更好的是,浏览有关以下部分的启用寄存器的描述 启用寄存器。 我们可以在哪里找到 RCC_APB1ENR 和 RCC_APB2ENR:
因此,它们包含 SPI2、IOPB(I/O 端口 B)和替代功能 (AFIO) 时钟等位。
#define _APB2ENR 0x18
#define _APB1ENR 0x1C
#define IOPBEN 0x0008
#define SPI2EN 0x4000
#define AFIOEN 0x0001
//включаем тактирование порта B и альт. функций
_RCC_(_APB2ENR) |= IOPBEN | AFIOEN;
//включаем тактирование SPI2
_RCC_(_APB1ENR) |= SPI2EN;
最终代码可以找到
如果您有机会并且希望进行测试,请像这样连接 DM634:DAI 到 PB15,DCK 到 PB13,LAT 到 PB14。 我们用 5 伏为驱动器供电,不要忘记连接地线。
STM8 脉宽调制
STM8上的脉宽调制
当我刚刚准备写这篇文章时,我决定,作为一个例子,尝试仅使用数据表来掌握不熟悉的芯片的一些功能,这样我就不会成为一个没有靴子的鞋匠。 STM8 非常适合这个角色:首先,我有几个带有 STM8S103 的中国板,其次,它不是很受欢迎,因此在互联网上阅读和寻找解决方案的诱惑在于缺乏这些解决方案。
该芯片还具有
时钟和 I/O
默认情况下,STM8 的工作频率为 2 MHz,必须立即纠正。
HSI(高速内部)时钟
HSI 时钟信号源自具有可编程分频器(16 至 1)的内部 8 MHz RC 振荡器。 它在时钟分频寄存器(CLK_CKDIVR)中设置。
注意:开始时,选择分频器为 8 的 HSI RC 振荡器作为时钟信号的主要源。
我们在datasheet中找到寄存器地址,refman中的描述,看到该寄存器需要清零:
#define CLK_CKDIVR *(volatile uint8_t *)0x0050C6
CLK_CKDIVR &= ~(0x18);
由于我们要运行 PWM 并连接 LED,所以让我们看一下引脚排列:
该芯片很小,许多功能都悬挂在同一引脚上。 方括号里的是“替代功能”,通过“选项字节”切换(选项字节) – 像 Atmega 保险丝之类的东西。 您可以通过编程方式更改它们的值,但这不是必需的,因为新功能仅在重新启动后才会激活。 使用 ST Visual Programmer(与 Visual Develop 一起下载)更容易,它可以更改这些字节。 引脚排列显示,第一个定时器的CH1和CH2引脚隐藏在方括号内; 需要设置STVP中的AFR1和AFR0位,第二个也将第二个定时器的CH1输出从PD4传输到PC5。
因此,6 个引脚将控制 LED:PC6、PC7 和 PC3 用于第一个定时器,PC5、PD3 和 PA3 用于第二个定时器。
在 STM8 上设置 I/O 引脚本身比在 STM32 上更简单、更符合逻辑:
- 熟悉 Atmega DDR 数据方向寄存器(数据方向寄存器):1 = 输出;
- 第一控制寄存器CR1输出时设置推挽模式(1)或开漏模式(0); 因为我用阴极将 LED 连接到芯片,所以我在这里留下了 XNUMX;
- 第二个控制寄存器 CR2 在输出时设置时钟速度:1 = 10 MHz
#define PA_DDR *(volatile uint8_t *)0x005002
#define PA_CR2 *(volatile uint8_t *)0x005004
#define PD_DDR *(volatile uint8_t *)0x005011
#define PD_CR2 *(volatile uint8_t *)0x005013
#define PC_DDR *(volatile uint8_t *)0x00500C
#define PC_CR2 *(volatile uint8_t *)0x00500E
PA_DDR = (1<<3); //output
PA_CR2 |= (1<<3); //fast
PD_DDR = (1<<3); //output
PD_CR2 |= (1<<3); //fast
PC_DDR = ((1<<3) | (1<<5) | (1<<6) | (1<<7)); //output
PC_CR2 |= ((1<<3) | (1<<5) | (1<<6) | (1<<7)); //fast
脉宽调制设置
首先,让我们定义术语:
- 脉宽调制频率 – 计时器计时的频率;
- 自动装弹、AR – 定时器计数的可自动加载值(脉冲周期);
- 更新事件,UEV – 当计时器计数到 AR 时发生的事件;
- 脉宽调制占空比 – PWM 占空比,通常称为“占空因数”;
- 捕获/比较值 – 定时器已计数的捕获/比较值 会做某事 (在 PWM 的情况下,它反转输出信号);
- 预载值 – 预载值。 比较值 当定时器计时时不能改变,否则 PWM 周期将中断。 因此,新传输的值被放置在缓冲区中,并在计时器到达倒计时结束时被拉出并重置;
- 边缘对齐 и 中心对齐模式 – 沿边界和中心对齐,与 Atmel 的相同 快速PWM и 相位校正 PWM.
- OCiREF,输出比较参考信号 – 参考输出信号,实际上是 PWM 模式下相应引脚上出现的信号。
从引脚排列中可以清楚地看出,两个定时器具有 PWM 功能——第一个和第二个。 两者都是 16 位,第一个有很多附加功能(特别是它可以向上和向下计数)。 我们需要两者平等地工作,所以我决定从明显较差的第二个开始,以免意外使用不存在的东西。 一些问题是,参考手册中所有定时器的 PWM 功能的描述都在有关第一个定时器(17.5.7 PWM 模式)的章节中,因此您必须始终在整个文档中来回跳转。
STM8上的PWM比Atmega上的PWM有一个重要的优势:
边界对齐 PWM
账户配置从下到上
如果 TIM_CR1 寄存器中的 DIR 位清零,则自下而上计数有效
例子
该示例使用第一种 PWM 模式。 只要 TIM1_CNT < TIM1_CCRi,PWM 参考信号 OCiREF 就会保持高电平。 否则需要低水平。 如果 TIM1_CCRi 寄存器中的比较值大于自动加载值(TIM1_ARR 寄存器),则 OCiREF 信号保持为 1。 如果比较值为 0,则 OCiREF 保持为零。...
STM8定时器期间 更新事件 首先检查 比较值,然后才产生参考信号。 Atmega 的计时器首先出错,然后进行比较,结果是 compare value == 0
输出是一根针,必须以某种方式处理它(例如,通过以编程方式反转逻辑)。
那么我们想要做的是:8位PWM(AR == 255
),从下往上数,沿边框对齐。 由于灯泡通过阴极连接到芯片,因此 PWM 应输出 0(LED 亮起),直到 比较值 和 1 之后。
我们已经读过一些 PWM模式,因此我们通过在参考手册中搜索这句话(18.6.8 - TIMx_CCMR1)来找到第二个定时器所需的寄存器:
110:第一 PWM 模式——从下到上计数时,当 TIMx_CNT < TIMx_CCR1 时,第一个通道有效。 否则,第一通道处于非活动状态。 [文档中还有来自定时器 1 的错误复制粘贴] 111:第二个 PWM 模式 – 从下到上计数时,当 TIMx_CNT < TIMx_CCR1 时,第一个通道处于非活动状态。 否则,第一通道处于活动状态。
由于 LED 通过阴极连接到 MK,因此第二种模式适合我们(第一种模式也是如此,但我们还不知道)。
位 3 OC1PE:使能引脚 1 预载
0:TIMx_CCR1 上的预载寄存器被禁用。 您可以随时写入 TIMx_CCR1。 新值立即生效。
1:TIMx_CCR1 上的预载寄存器使能。 读/写操作访问预载寄存器。 预加载值 TIMx_CCR1 在每次更新事件期间加载到影子寄存器中。
*注:为了使 PWM 模式正常工作,必须使能预载寄存器。 这在单信号模式下是不必要的(OPM 位在 TIMx_CR1 寄存器中设置)。
好的,让我们打开第二个计时器的三个通道所需的一切:
#define TIM2_CCMR1 *(volatile uint8_t *)0x005307
#define TIM2_CCMR2 *(volatile uint8_t *)0x005308
#define TIM2_CCMR3 *(volatile uint8_t *)0x005309
#define PWM_MODE2 0x70 //PWM mode 2, 0b01110000
#define OCxPE 0x08 //preload enable
TIM2_CCMR1 = (PWM_MODE2 | OCxPE);
TIM2_CCMR2 = (PWM_MODE2 | OCxPE);
TIM2_CCMR3 = (PWM_MODE2 | OCxPE);
AR由两个八位寄存器组成,一切都很简单:
#define TIM2_ARRH *(volatile uint8_t *)0x00530F
#define TIM2_ARRL *(volatile uint8_t *)0x005310
TIM2_ARRH = 0;
TIM2_ARRL = 255;
第二个定时器只能从下往上计数,沿着边框对齐,不需要做任何改变。 让我们将分频器设置为 256。对于第二个定时器,分频器在 TIM2_PSCR 寄存器中设置,并且是 XNUMX 的幂:
#define TIM2_PSCR *(volatile uint8_t *)0x00530E
TIM2_PSCR = 8;
剩下的就是打开结论和第二个计时器本身。 第一个问题通过寄存器解决 捕获/比较 启用:有两三个通道不对称地分布在其中。 在这里我们还可以了解到可以改变信号的极性,即原则上,可以使用 PWM 模式 1。我们写道:
#define TIM2_CCER1 *(volatile uint8_t *)0x00530A
#define TIM2_CCER2 *(volatile uint8_t *)0x00530B
#define CC1E (1<<0) // CCER1
#define CC2E (1<<4) // CCER1
#define CC3E (1<<0) // CCER2
TIM2_CCER1 = (CC1E | CC2E);
TIM2_CCER2 = CC3E;
最后,我们启动 TIMx_CR1 寄存器中的定时器:
#define TIM2_CR1 *(volatile uint8_t *)0x005300
TIM2_CR1 |= 1;
让我们编写一个 AnalogWrite() 的简单模拟,它将实际值传输到计时器进行比较。 寄存器的命名是可预测的 捕获/比较寄存器,每个通道有两个:TIM8_CCRxL 中的低位 2 位和 TIM2_CCRxH 中的高位。 由于我们创建了 8 位 PWM,因此仅写入最低有效位就足够了:
#define TIM2_CCR1L *(volatile uint8_t *)0x005312
#define TIM2_CCR2L *(volatile uint8_t *)0x005314
#define TIM2_CCR3L *(volatile uint8_t *)0x005316
void setRGBled(uint8_t r, uint8_t g, uint8_t b)
{
TIM2_CCR1L = r;
TIM2_CCR2L = g;
TIM2_CCR3L = b;
}
细心的读者会注意到,我们的 PWM 略有缺陷,无法产生 100% 填充(最大值为 255,信号在一个定时器周期内反转)。 对于 LED 来说,这并不重要,细心的读者已经可以猜到如何解决它。
第二个定时器上的 PWM 工作正常,让我们继续讨论第一个定时器。
第一个定时器在相同的寄存器中具有完全相同的位(只是第二个定时器中保留“保留”的那些位在第一个定时器中积极用于各种高级功能)。 因此,在数据表中找到相同寄存器的地址并复制代码就足够了。 好吧,改变分频器的值,因为...... 第一个定时器想要接收的不是 16 的幂,而是两个寄存器中的精确 XNUMX 位值 预分频器高 и 低。 我们做了一切,但...第一个计时器不起作用。 怎么了?
这个问题只能通过查看关于定时器1的控制寄存器的整个部分来解决,我们在其中寻找第二个定时器没有的控制寄存器。 将有 17.7.30 中断寄存器(TIM1_BKR),其中有这一点:
启用主输出
#define TIM1_BKR *(volatile uint8_t *)0x00526D
TIM1_BKR = (1<<7);
现在一切都确定了,代码
STM8 复用
STM8 上的复用
第三个迷你项目是将八个RGB LED以PWM模式连接到第二个定时器,并使它们显示不同的颜色。 它基于 LED 多路复用的概念,即如果您非常非常快地打开和关闭 LED,我们会觉得它们一直处于打开状态(视觉的坚持,视觉感知惯性)。 我曾经做过
工作算法如下所示:
- 连接第一个RGB LED的阳极;
- 点燃它,向阴极发送必要的信号;
- 等到PWM周期结束;
- 连接第二个RGB LED的阳极;
- 点燃它...
嗯,等等。 当然,为了实现良好的操作,需要连接阳极并同时“点亮”LED。 嗯,或者说差不多了。 无论如何,我们需要编写一段代码,在第二个定时器的三个通道中输出值,在达到 UEV 时更改它们,同时更改当前活动的 RGB LED。
由于 LED 切换是自动的,因此我们需要创建一个“视频存储器”,中断处理程序将从中接收数据。 这是一个简单的数组:
uint8_t colors[8][3];
为了改变特定 LED 的颜色,只需将所需的值写入该数组即可。 该变量将负责激活 LED 的数量
uint8_t cnt;
解复用器
奇怪的是,为了实现正确的多路复用,我们需要一个 CD74HC238 解复用器。 解复用器 - 在硬件中实现运算符的芯片 <<
。 通过三个输入引脚(位 0、1 和 2),我们向其提供一个三位数字 X,作为响应,它激活输出数字(1<<X
)。 芯片的其余输入用于扩展整个设计。 我们需要这个芯片不仅是为了减少微控制器占用的引脚数量,也是为了安全——以免意外打开过多的LED,也不会烧毁MK。 该芯片售价一美分,应始终存放在您的家庭药柜中。
我们的 CD74HC238 将负责向所需 LED 的阳极提供电压。 在成熟的多路复用中,它将通过 P-MOSFET 向列提供电压,但在本演示中可以直接提供,因为它消耗 20 mA 的电流,根据 绝对最大额定值 在数据表中。 从
H = 高电压电平,L = 低电压电平,X – 无关
我们将E2和E1接地,将E3、A0、A1和A3连接到STM5的引脚PD3、PC4、PC5和PC8。 由于上表包含低电平和高电平,因此我们将这些引脚配置为推挽引脚。
脉宽调制
第二个定时器上的 PWM 的配置方式与前面的故事相同,但有两点不同:
首先,我们需要开启中断 更新事件 (UEV),它将调用一个切换活动 LED 的函数。 这是通过改变位来完成的 更新中断使能 在一个有明显名字的登记册中
中断使能寄存器
#define TIM2_IER *(volatile uint8_t *)0x005303
//enable interrupt
TIM2_IER = 1;
第二个区别与复用现象有关,比如 重影 – 二极管的寄生辉光。 在我们的例子中,出现这种情况的原因可能是定时器在 UEV 上引起了中断,并继续计时,并且中断处理程序没有时间在定时器开始向引脚写入内容之前切换 LED。 为了解决这个问题,您必须反转逻辑(0 = 最大亮度,255 = 没有亮起)并避免极端的占空比值。 那些。 确保 UEV 之后 LED 在一个 PWM 周期内完全熄灭。
改变极性:
//set polarity
TIM2_CCER1 |= (CC1P | CC2P);
TIM2_CCER2 |= CC3P;
避免将 r、g 和 b 设置为 255,并记住在使用它们时反转它们。
中断
中断的本质是在某种情况下芯片停止执行主程序并调用一些外部函数。 中断是由于外部或内部影响(包括定时器)而发生的。
当我们第一次在ST Visual Develop中创建一个项目时,除了 main.c
我们收到一个带有神秘文件的窗口 stm8_interrupt_vector.c
,自动包含在项目中。 在这个文件中,为每个中断分配了一个函数 NonHandledInterrupt
。 我们需要将我们的函数绑定到所需的中断。
数据表中有一个中断向量表,我们可以在其中找到我们需要的中断向量:
13 TIM2 更新/溢出
14 TIM2 捕获/比较
我们需要更改 UEV 处的 LED,因此需要中断#13。
因此,首先在文件中 stm8_interrupt_vector.c
将负责 13 号中断 (IRQ13) 的函数的默认名称更改为您自己的:
{0x82, TIM2_Overflow}, /* irq13 */
其次,我们必须创建一个文件 main.h
包含以下内容:
#ifndef __MAIN_H
#define __MAIN_H
@far @interrupt void TIM2_Overflow (void);
#endif
最后,将此函数写入您的 main.c
:
@far @interrupt void TIM2_Overflow (void)
{
PD_ODR &= ~(1<<5); // вырубаем демультиплексор
PC_ODR = (cnt<<3); // записываем в демультиплексор новое значение
PD_ODR |= (1<<5); // включаем демультиплексор
TIM2_SR1 = 0; // сбрасываем флаг Update Interrupt Pending
cnt++;
cnt &= 7; // двигаем счетчик LED
TIM2_CCR1L = ~colors[cnt][0]; // передаем в буфер инвертированные значения
TIM2_CCR2L = ~colors[cnt][1]; // для следующего цикла ШИМ
TIM2_CCR3L = ~colors[cnt][2]; //
return;
}
剩下的就是启用中断。 这是使用汇编命令完成的 rim
- 你必须在以下位置寻找它
//enable interrupts
_asm("rim");
另一个汇编命令是 sim
– 关闭中断。 在将新值写入“视频内存”时必须关闭它们,以便在错误时刻引起的中断不会破坏数组。
所有代码 -
如果至少有人觉得这篇文章有用,那我就没有白写。 我很高兴收到评论和评论,我会尽力回答所有问题。
来源: habr.com