论时序要求的重要性(移位寄存器控制数码管)


1. 问题背景

硬件背景

  • 移位寄存器:SN74HC595N
  • 两个4位共阳数码管

移位/锁存逻辑——set/reset

在学习GD32F407VET时,将学习STC8实现的数码管模块移植过来,发现了一个很奇怪的问题。其中移位操作的实现如下(封装了对两个串联移位寄存器的移位操作,控制8个数码管中显示哪一个,以及控制数码管显示内容):

static void shift(uint8_t data) {
    for (uint8_t i = 0; i < 8; ++i) {
        gpio_bit_write(NIX_DI_PORT, NIX_DI_PIN, (data & (0x80 >> i)) ? SET : RESET);
        gpio_bit_reset(NIX_SCK_PORT, NIX_SCK_PIN);
        __NOP();
        gpio_bit_set(NIX_SCK_PORT, NIX_SCK_PIN);
        __NOP();
    }
}

static void rck_action() {
    gpio_bit_reset(NIX_RCK_PORT, NIX_RCK_PIN);
    __NOP();
    gpio_bit_set(NIX_RCK_PORT, NIX_RCK_PIN);
    __NOP();
}

发现单独指定某个数码管显示某个数是没有问题的:

// 要显示1~8对应的码表
uint8_t code[] = {0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80,};
Int_NixieTube_DisplaySingle(0, code[0]);

但是想使用 for控制数码管轮流显示1~8时,就发现显示的内容并不符合预期:

for (uint8_t i = 0; i < 8; ++i) {
    Int_NixieTube_DisplaySingle(i, code[i]);
    delay_1ms(1000);
}

移位/锁存逻辑——write

但是将其中 gpio_bitset/reset换成 write之后,发现数码管能够按照预期显示了:

static void shift(uint8_t data) {
    for (uint8_t i = 0; i < 8; ++i) {
        gpio_bit_write(NIX_DI_PORT, NIX_DI_PIN, (data & (0x80 >> i)) ? SET : RESET);
        gpio_bit_write(NIX_SCK_PORT, NIX_SCK_PIN, RESET);
        __NOP();
        gpio_bit_write(NIX_SCK_PORT, NIX_SCK_PIN, SET);
        __NOP();
    }
}

static void rck_action() {
    gpio_bit_write(NIX_RCK_PORT, NIX_RCK_PIN, RESET);
    __NOP();
    gpio_bit_write(NIX_RCK_PORT, NIX_RCK_PIN, SET);
    __NOP();
}

查看这些函数的实现:

void gpio_bit_set(uint32_t gpio_periph, uint32_t pin)
{
    GPIO_BOP(gpio_periph) = (uint32_t)pin;
}

void gpio_bit_reset(uint32_t gpio_periph, uint32_t pin)
{
    GPIO_BC(gpio_periph) = (uint32_t)pin;
}

void gpio_bit_write(uint32_t gpio_periph, uint32_t pin, bit_status bit_value)
{
    if(RESET != bit_value) {
        GPIO_BOP(gpio_periph) = (uint32_t)pin;
    } else {
        GPIO_BC(gpio_periph) = (uint32_t)pin;
    }
}

发现 write只不过将 set/reset包成了一个函数,唯一不同的就是多了一个 if判断。

但是,一个if 的耗时又能对程序产生什么影响呢?我百思不得其解。

2. 时序要求引发的血案

小小if暗藏玄机

在各种 Google, GPT之后,结合代码上下文,发现代码中使用了 nop来增加时延,再结合移位寄存器需要根据我们通过GPIO发送的SRCLK(移位时钟)、RCLK(锁存时钟)的上升沿来进行移位操作和锁存操作(将移位寄存器更新到锁存寄存器 storage register)。

我设置的GD32F407的主频是168MHz,这个比STC8时的24MHz还是要快几倍的,计算一下nop对应一个时钟周期的时间为 1/168MHz约为 5.95ns1/24MHz约为 41.67ns

这样看来,一个 if判断还真有可能引发了血案,其所消耗的时钟周期(增加的时延)可能正好满足了移位寄存器的时序要求,从而使得数码管能够正常显示。

数据手册不可少

在想到可能时时延导致的问题后,不妨看一下芯片对应的官方手册,看能否找到答案。

这里要注意的是,一定要找与芯片型号、品牌一致的:

image-20241115170556369

Timing Requirements

这里我们主要看时序要求(Timing Requirements )相关的章节

image-20241115170729345

Set-up time

其中描述了,我们在使用串行信号线 SER、移位寄存器时钟线 SRCLK、锁存器时钟线 RCLK 来操作 SN74HC595N 时,需要的准备时间,例如

SER before SRCLK↑

在操作SRCLK上升沿将SER存入移位寄存器之前,SER应该预备的时间,以125ns为例,伪代码如下

set SER
wait 125ns
SRCLK = 0;
SRCLK = 1;

SRCLK↑ before RCLK↑

在操作完所有的移位后,将移位寄存器更新到锁存寄存器(即更新到电路,控制数码管的段选和片选),需要操作RCLK上升沿。

该参数规定了,RCLK上升沿应该与SRCLK上升沿保持的时间间隔

Pulse duration

其中描述了SRCLK、RCLK被置位后应该持续一段时间,所以我们还需要在上述基础上增加两个延时(以100ns为例)

set SER
wait 125ns
SRCLK = 0;
wait 100ns
SRCLK = 1;

再加上锁存的操作:

set SER
wait 125ns
SRCLK = 0;
wait 100ns
SRCLK = 1;

wait 100ns
RCLK = 0;
wait 100ns
RCLK = 1;

再看if和nop

之前对于时序控制的理解并不深刻,简单的以为使用 nop停顿一下就好。现在看来,无论是不同主频对应的 nop时延不同,还是 if耗时也能影响数码管的生死,都在提醒我们时序控制不可小觑。

在使用MCU对外围设备/芯片交互

、控制时,一定要严格按照芯片要求的时序控制,结合MCU自身指令耗时来编写程序。

static void shift(uint8_t data) {
    for (uint8_t i = 0; i < 8; ++i) {
        gpio_bit_write(NIX_DI_PORT, NIX_DI_PIN, (data & (0x80 >> i)) ? SET : RESET);
        delay_1us(1);
        gpio_bit_reset(NIX_SCK_PORT, NIX_SCK_PIN);
        delay_1us(1);
        gpio_bit_set(NIX_SCK_PORT, NIX_SCK_PIN);
        delay_1us(1);
    }
}

static void rck_action() {
    gpio_bit_reset(NIX_RCK_PORT, NIX_RCK_PIN);
    delay_1us(1);
    gpio_bit_set(NIX_RCK_PORT, NIX_RCK_PIN);
    delay_1us(1);
}

这里宁愿多等一点,也不要让 SN74HC595N 无法准确移位、锁存数据。


文章作者: 安文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 安文 !
  目录