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_bit
的 set/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.95ns
、1/24MHz
约为 41.67ns
。
这样看来,一个 if
判断还真有可能引发了血案,其所消耗的时钟周期(增加的时延)可能正好满足了移位寄存器的时序要求,从而使得数码管能够正常显示。
数据手册不可少
在想到可能时时延导致的问题后,不妨看一下芯片对应的官方手册,看能否找到答案。
这里要注意的是,一定要找与芯片型号、品牌一致的:
Timing Requirements
这里我们主要看时序要求(Timing Requirements )相关的章节
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 无法准确移位、锁存数据。