本月刚开始的时候,由于学校里教的东西实在是无聊,在找一些新的事情做;@t123yh 劝我学习单片机,从此便入了单片机的坑。我先买了个 arduino Uno R3 官方开发板,以及一些 esp8266、esp32 的板子,在上面用 arduino 和 micropython 写了些程序。但 arduino 是一个 toy model,接口抽象程度非常高,方便了新手的学习,但不可避免地掩盖了底层的功能;micropython 亦然。采用 arduino 很难组织稍微大一点的项目,迫使我考虑其他开发板和开发环境。

  esp32 有官方 C++ SDK,叫 esp-idf,搭建起来本身就是个麻烦事。我在 windows 下始终不得要领,于是切换到 debian 上装 esp-idf。装上之后用 VS Code 写代码确实顺利,不过很缺乏 IDE 的支持。加之 esp32 虽然是个好模块,但库和框架都少,对我这种新人的学习可能不利,后来我又买了点 stm32 的板子,用 Jetbrains 的 CLion 开发程序,终于觉得很方便了。今天把途中遇到的坑写一下。

0x01 环境配置

  这个开发环境还是很容易配的,Linux 下就不说了,win10 可以采用 choco 这个程序把 OpenOCD 和 gcc-arm-embedded 装上。但有一个大坑:choco 虽然可以通过软件包 mingw-w64 装上 gcc/g++,但没有带 gdb!我也不知道软件源维护者的脑子里进了什么东西,总之评论区全是吐槽:

  最后我用 mingw 官方的程序装上了 gcc 全家桶。gcc 版本很旧,只有 6.x(与之对比,mingw-w64 的版本是 8.x),但够我用来开发单片机了。总之折腾一下午之后终于配好了环境。

  顺便说一句为什么不用官方的 STM32CubeIDE。我买了几块 stm32 的板子,其中只有一块 F3 Discovery 板子是官方版,自带 ST-LINK V2;另外几块板子是需要外部 ST-LINK 的。我在淘宝买了个寨版 ST-LINK,发现栽了跟头:寨版固件版本是 V2.J17.S4,而想用 STM32CubeIDE 进行调试,必须先把 ST-LINK 固件升级到 V2.J37.S7,升级失败。看了一下淘宝评价,发现这寨版根本不能升。于是放弃了 STM32CubeIDE,去寻替代品。Jetbrains 家的 IDE 我用得挺熟练的,所以选了 CLion 来开发。

0x02 初始化

  CLion 采用 CubeMX 来生成初始代码,交互上做得还很不好。总之我们先点击左上角的 MCU 型号,把板子换成 F3 Discovery;于是原来的项目名就木大了,变成「Untitled」。给项目改名(要和 CLion 项目的名字一模一样),最终如下:

▲ CLion 的项目名误写成了「74hc595」,这边也只能将错就错

  然后配置 GPIO 用途。我们把 PB12PB13PB14 用于数字信号输出。

0x03 关于 74LS595

  我们的目标是控制 8 个 LED 的通断。如果按一般的做法,那就是从单片机上的 8 个 GPIO 各引一条线到对应的 LED。然而 MCU 上的 GPIO 是很宝贵的,这样一次性占 8 个 GPIO 对很多应用场景来说,都不合适。

  首先说一下这里为什么用 74LS 芯片而不用 74HC 芯片。

  1. 我打算用 3.3V 的电源去驱动芯片和 LED,而 74HC 芯片是 5V 电平,虽然逻辑上也能工作,但我毕竟第一次用这块芯片,还是准备按标准来。
  2. 74LS 芯片默认上拉,管脚悬空视为高电平。这样我可以少接几根线。

  来看 74LS595 的管脚定义:

▲ 74LS595 datasheet,图源 TI 官网

  我手上的芯片是右上角那种。各个引脚的定义如下:

  • QA-QH 是数据输出,我用来控制 8 个 LED。
  • SER 是数据输入。
  • SRCK 是时钟信号,上升沿触发。触发后,把 SER 的值压进移位寄存器中。这里只是把数据压进 shift register 里面去,并不改变输出。
  • RCK 也是上升沿触发的时钟信号,触发后用 shift register 的值覆盖掉 storage register,改变芯片的输出。
  • QH' 是级联输出,如果级联多个 74LS595 的话,把 QH' 连到下一个芯片的 SER 管脚,就能把自己移位时抛掉的 bit 送进下一个芯片。我这里只需要操控 8 个 LED,不用级联,故把这个管脚悬空。
  • G 在高电平时阻止输出(把输出变成高阻态),换句话讲就是低电平的时候使能输出端。我们希望 LED 常亮,所以 G 应该接到 GND 来拉低。
  • SRCLR 是低电平有效的清零端,方便我们用程序直接清零芯片(而不是连续压 8 个 0 进去)。不过我们这里为了节省 GPIO,不采用这个清零渠道,故把 SRCLR 拉到 VCC。

  74LS595 提供了「锁定输出端」的功能,这是为了防止在移位过程中产生的各种中间状态被输出。我们之后会演示这个情形。这个特性是我们选择 74LS595 而非普通移位寄存器的主要原因。顺便一提,SN74LS595 的官方描述是「Shift Registers With Output Latches」,带输出锁的移位寄存器。

  现在我想把这 8 个 LED 设为一个状态(例如 10101010),需要干的事情如下:

  1. SER 置为对应的信号。
  2. 做一个 SRCK 的上升沿,把刚刚设置的信号压进去。
  3. 重复上述过程 8 次,于是 shift register 里面变成了我们期望的那 8 位数据。做一个 RCK 的上升沿,来把 storage register 改成这 8 位,从而点亮我们期望的那些 LED。

  原理很简单。接下来开始焊板子。

0x04 焊 武 帝

  至于为什么不用面包板来做,是因为 74LS595 芯片是 DIP16 封装,而我的 LED 的直径大于 100mil(2.54mm),占用面包板的不止一列,所以如果要采用面包板,杜邦线必须飞得到处都是……最终决定在万能板上面飞线来做这个事。

  首先摆好芯片、LED 和排针,五个排针分别对应 GND, VCC, SER, RCK, SRCK ,然后开始焊。

  这个飞线采用的是漆包线,外面有一层绝缘膜,但是在焊点处是接通的,所以不用刮漆,非常好用。具体原理我也不知道,也许是加热过程会破坏外层漆吧……总之好用就完事了。然后连接板子和 stm32 开发板,最终是下面的样子:

0x05 写代码

  我们开始写代码。为了方便复用,我们写一个 led595.h 来定义几个函数:

  然后在 led595.c 里面实现之。注意 .h.c 的文件放置位置不一样,前者目录是 Core/Inc ,后者目录是 Core/Src 。在使用的时候,只需要包含 led595.h ,如下:

  具体实现如下,写在 led595.c 里面:

//
// Created by blue on 2021/5/26.
//

#include <main.h>

#define PIN_SER GPIO_PIN_12
#define PIN_RCK GPIO_PIN_13
#define PIN_SCK GPIO_PIN_14

#define MS_TO_DELAY 1

void set_led_state(unsigned state) {
    HAL_GPIO_WritePin(GPIOB, PIN_SCK, 0);       // 拉低 SCK、RCK,以便之后产生上升沿
    HAL_GPIO_WritePin(GPIOB, PIN_RCK, 0);

    for(int i=7; i>=0; i--)
    {
        HAL_GPIO_WritePin(GPIOB, PIN_SER, (state & (1<<i)) > 0);       // 写 state 的对应位

        HAL_GPIO_WritePin(GPIOB, PIN_SCK, 1);

        HAL_Delay(MS_TO_DELAY);
        HAL_GPIO_WritePin(GPIOB, PIN_SCK, 0);
        HAL_Delay(MS_TO_DELAY);
    }

    HAL_GPIO_WritePin(GPIOB, PIN_RCK, 1);   // 从移位寄存器的暂存值,输出到数据寄存器
    HAL_Delay(MS_TO_DELAY);
    HAL_GPIO_WritePin(GPIOB, PIN_RCK, 0);
}

void set_led_state_with_shifting(unsigned state) {
    HAL_GPIO_WritePin(GPIOB, PIN_SCK, 0);       // 拉低 SCK、RCK,以便之后产生上升沿
    HAL_GPIO_WritePin(GPIOB, PIN_RCK, 0);

    for(int i=7; i>=0; i--)
    {
        HAL_GPIO_WritePin(GPIOB, PIN_SER, (state & (1<<i)) > 0);       // 写 state 的对应位

        HAL_GPIO_WritePin(GPIOB, PIN_SCK, 1);

        HAL_Delay(MS_TO_DELAY);
        HAL_GPIO_WritePin(GPIOB, PIN_SCK, 0);
        HAL_Delay(MS_TO_DELAY);

        HAL_GPIO_WritePin(GPIOB, PIN_RCK, 1);   // 从移位寄存器的暂存值,输出到数据寄存器
        HAL_Delay(MS_TO_DELAY);
        HAL_GPIO_WritePin(GPIOB, PIN_RCK, 0);
    }
}

  我们这里演示了两种方法,set_led_state 是按照我们之前描述的方案来做,而 set_led_state_with_shifting 是模拟了一个纯粹的移位寄存器,直接把 shift register 的内容输出。为了演示这两种方案的不同,我们把 MS_TO_DELAY 设成几百毫秒。烧录程序,开跑:

0x06 两种移位寄存器方案的比较

  常规的移位寄存器是这样的:

  可见,为了输出 10101010 ,不得不经过一大堆中间状态。而带锁的移位寄存器是这样的:

  一步到位,非常文明!

0x07 二进制计时器

  KDE plasma 里面有个桌面部件叫做「二进制时钟」,拿像素块来显示时间,很有意思。我们这边的 LED 数量不够做二进制时钟,但是可以做个二进制计时器:LED 显示 cnt 的二进制值,每隔 300ms 执行一次 cnt++

  最终效果如图所示:

0x08 总结

  我们用三个输出端口(一个数据、两个时钟),完成了 8 个 LED 的控制。串口转并口要付出频率的代价,不过我们这个应用里面,频率除以 8 是毫无问题的。事实上,用这三个输出端口可以控制更多的 LED,只需要级联几块 74LS595。

  GPIO 直接连接 LED 的情况下,可以通过 PWM 来调控 LED 亮度。74LS595 的速度很快,我隐约感觉 PWM 调 8 个 LED 亮度是可行的。但是现在的代码跑得太慢,即使 MS_TO_DELAY 设成 0,计数 256 也要耗掉 2s 左右,算下来切换得最频繁的 LED0 大概才 100Hz,闪烁很明显。优化一下,改一改时钟频率,也许能达到 PWM 的要求,但是我要复习去了。复习完了再摸一摸罢。