之前接触过一些硬件描述语言以及VHDL/CPLD的单片机的设计实验,那时是2022年了
这里补写一篇笔记,以记录一下那十多个小时 万年历实验 研究中的心得体会:
说明解释都是个人理解,与标准描述有较大出入......
目录
输入输出器件的编写:
分频器点亮不同频率LED:
引用头文件:
实体(ENTITY)定义和架构(ARCHITECTURE)的开头部分:
主体:变量定义与时钟事件检查:
完整代码:
电路连接与效果描述:
级联分频/计时器实现时钟的效果:
四位计数器(10分频)的内部硬件描述代码:
接口与变量作用定义:
计数器过程:
数码管分频计数逻辑输出器:
编辑
它的完整代码如下:
最后的输入输出总线连接:
编译下载效果如下:
回忆与感悟 :
输入输出器件的编写:
想必大家都听说过一些单片机外围电路上的芯片:
比如74HC138(三选八)芯片,74HC573扩展芯片等,这些芯片在硬件上定义一些输入与输出相联系的逻辑来实现他们的功能,
而这里的编程则是在软件上自己先使用硬件描述语言进行对输入输出逻辑的组合,自己编写出类似于分频器、片选芯片类似功能的器件模块,然后再将它们与具体的输入输出线引脚相组合,最后编译无报错下载进板子查看效果的过程。
分频器点亮不同频率LED:
这里展示了一个编写简单的分频器的硬件描述语言的逻辑代码,下面分布解释:
引用头文件:
LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; USE IEEE.STD_LOGIC_UNSIGNED.ALL;
实体(
ENTITY
)定义和架构(ARCHITECTURE
)的开头部分:PORT 描述了一个具有时钟分离功能的模块,
它接收一个输入时钟信号
CLK
,并产生两个输出时钟信号:
CLK_1HZ
(1赫兹的时钟)和CLK_1KHZ
(1千赫兹的时钟)ENTITY CLK_SEP IS PORT(CLK:IN STD_LOGIC; CLK_1HZ:OUT STD_LOGIC; CLK_1KHZ:OUT STD_LOGIC); END CLK_SEP; ARCHITECTURE BHV OF CLK_SEP IS
主体:变量定义与时钟事件检查:
COUNT1
:一个24位的STD_LOGIC_VECTOR
,用于产生1Hz的时钟信号。
COUNT2
:一个14位的STD_LOGIC_VECTOR
,用于产生1kHz的时钟信号。
IF CLK'EVENT AND CLK = '1' THEN
:这一行检查CLK
信号是否有一个事件(上升沿0变为1)
COUNT1
与COUNT2
在每个CLK
上升沿递增。当
COUNT1
的值在"010110111000110110000000"
(二进制表示,对应于某个十进制数)和"101101110001101100000000"
之间时,CLK_1HZ
被设置为'1'
。这个范围代表了从某个特定开始点到结束点的时间段,用于产生1Hz的时钟周期。当
COUNT1
的值小于或等于结束点且大于或等于"000000000000000000000000"
(即开始点)时,CLK_1HZ
被设置为'0'
。当
COUNT1
达到结束点"101101110001101100000000"
时,它被重置为"000000000000000000000000"
,以便开始下一个1Hz周期。当
COUNT2
的值在"01011101110000"
和"10111011100000"
之间时,CLK_1KHZ
被设置为'1'
。这个范围代表了从某个特定开始点到结束点的时间段,用于产生1kHz的时钟周期。当
COUNT2
的值小于或等于结束点且大于或等于"00000000000000"
时,CLK_1KHZ
被设置为'0'
。当
COUNT2
达到结束点"10111011100000"
时,它被重置为"00000000000000"
,以便开始下一个1kHz周期。BEGIN PROCESS(CLK) VARIABLE COUNT1:STD_LOGIC_VECTOR(23 DOWNTO 0); VARIABLE COUNT2:STD_LOGIC_VECTOR(13 DOWNTO 0); BEGIN IF CLK'EVENT AND CLK = '1' THEN COUNT1:=COUNT1+1; COUNT2:=COUNT2+1; IF ((COUNT1>="010110111000110110000000") AND (COUNT1<="101101110001101100000000") ) THEN CLK_1HZ<='1'; ELSIF ((COUNT1<="010110111000110110000000") AND(COUNT1>="000000000000000000000000")) THEN CLK_1HZ<='0'; IF COUNT1="101101110001101100000000" THEN COUNT1:="000000000000000000000000"; END IF; END IF; IF ((COUNT2>="01011101110000") AND (COUNT2<="10111011100000")) THEN CLK_1KHZ<='1'; ELSIF ((COUNT2<="01011101110000")AND(COUNT2>="00000000000000")) THEN CLK_1KHZ<='0'; IF(COUNT2="10111011100000") THEN COUNT2:="00000000000000"; END IF; END IF; END IF; END PROCESS; END BHV;
完整代码:
LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; USE IEEE.STD_LOGIC_UNSIGNED.ALL; ENTITY CLK_SEP IS PORT(CLK:IN STD_LOGIC; CLK_1HZ:OUT STD_LOGIC; CLK_1KHZ:OUT STD_LOGIC); END CLK_SEP; ARCHITECTURE BHV OF CLK_SEP IS BEGIN PROCESS(CLK) VARIABLE COUNT1:STD_LOGIC_VECTOR(23 DOWNTO 0); VARIABLE COUNT2:STD_LOGIC_VECTOR(13 DOWNTO 0); BEGIN IF CLK'EVENT AND CLK = '1' THEN COUNT1:=COUNT1+1; COUNT2:=COUNT2+1; IF ((COUNT1>="010110111000110110000000") AND (COUNT1<="101101110001101100000000") ) THEN CLK_1HZ<='1'; ELSIF ((COUNT1<="010110111000110110000000") AND(COUNT1>="000000000000000000000000")) THEN CLK_1HZ<='0'; IF COUNT1="101101110001101100000000" THEN COUNT1:="000000000000000000000000"; END IF; END IF; IF ((COUNT2>="01011101110000") AND (COUNT2<="10111011100000")) THEN CLK_1KHZ<='1'; ELSIF ((COUNT2<="01011101110000")AND(COUNT2>="00000000000000")) THEN CLK_1KHZ<='0'; IF(COUNT2="10111011100000") THEN COUNT2:="00000000000000"; END IF; END IF; END IF; END PROCESS; END BHV;
电路连接与效果描述:
最终是实现了俩个LED不同频率的亮灭:
级联分频/计时器实现时钟的效果:
有人在设计时钟时想得十分麻烦,又是10分频器,又是6分频器,又是12分频,又是60分...
其实时间的进位关系是一目了然的,
我们只需要给每个分频器写一个到达某个特定时分进位的逻辑,然后将这个逻辑进行级联,即可只靠俩种分频器即可做到时钟的效果:
这里我明显当年没考虑到一天是有24小时的,毕竟是个大学的课设,并未想得做得完善
但其中器件的逻辑还是设计得清晰的:
Q_OUT 用于分频器后的进位级联,计数到分频计数器逻辑定义的极限
(10或者6 )时就会在这个输出口产生一个进位信号,然后分频计数器
就会 重新开始计数
CNT[0]~CNT[3] 是一个提供当前分频/计数器 计数值的总线,(二进制表示0~9)
en 是一个使能,直接连接了VCC,(其实可以不定义这个输入)
但定义了以后可以实现其他复杂的片选功能,实现想开就开,想关就关
的灵活分频计时功能把
四位计数器(10分频)的内部硬件描述代码:
LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; USE IEEE.STD_LOGIC_UNSIGNED.ALL; ENTITY COUNT_10 IS PORT(clk,clr,en:IN STD_LOGIC; qa1,qb1,qc1,qd1,Q_OUT:OUT STD_LOGIC); END COUNT_10; ARCHITECTURE rtl OF COUNT_10 IS SIGNAL count_4:STD_LOGIC_VECTOR(3 DOWNTO 0); BEGIN qa1<=count_4(0); qb1<=count_4(1); qc1<=count_4(2); qd1<=count_4(3); PROCESS(clk,clr) BEGIN IF(clr='1')THEN count_4<="0000"; ELSIF(clk'EVENT AND clk='1')THEN IF(en='1')THEN IF(count_4="1001")THEN count_4<="0000";Q_OUT<='1'; ELSE count_4<=count_4+'1';Q_OUT<='0'; END IF; END IF; END IF; END PROCESS; END rtl;
接口与变量作用定义:
PORT(clk,clr,en:IN STD_LOGIC;qa1,qb1,qc1,qd1,Q_OUT:OUT STD_LOGIC);
定义器件包含的输入输出接口
SIGNAL count_4:STD_LOGIC_VECTOR(3 DOWNTO 0);
:定义了一个四位的
STD_LOGIC_VECTOR
信号count_4
,用于存储计数器的值。
qa1
,qb1
,qc1
,qd1
分别被赋值为count_4
的四位计数器过程:
当
clr
为高电平时,计数器count_4
被清零。当
clk
的上升沿到来(clk'EVENT AND clk='1'
)且en
为高电平时,计数器进行更新:如果
count_4
的值为1001
(即十进制的10),则计数器被清零,并且Q_OUT
输出高电平。否则,计数器加1,并且Q_OUT
输出低电平。
数码管分频计数逻辑输出器:
最后还有个原始接收1khz信号,以及各级分频计时器的 CNT[0]~CNT[3] 计时输出来对数码管进行段选和位选的逻辑器件,它也是需要接收一个clk时钟信号进行段选位选的
它的完整代码如下:
LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; USE IEEE.STD_LOGIC_UNSIGNED.ALL; ENTITY SMG_WXQ IS PORT( CLK:IN STD_LOGIC; C_IN1:IN STD_LOGIC_VECTOR(3 DOWNTO 0); C_IN2:IN STD_LOGIC_VECTOR(3 DOWNTO 0); C_IN3:IN STD_LOGIC_VECTOR(3 DOWNTO 0); C_IN4:IN STD_LOGIC_VECTOR(3 DOWNTO 0); C_OUNT:BUFFER STD_LOGIC_VECTOR(6 DOWNTO 0); SMG_WX:out std_logic_vector(7 downto 0) ); END SMG_WXQ; ARCHITECTURE rtl OF SMG_WXQ IS BEGIN process(clk) variable cnt : integer range 0 to 3 := 0; BEGIN IF CLK'EVENT AND CLK = '1' THEN IF cnt=0 THEN SMG_WX<="00000001"; case C_IN1 is when "0000"=>C_OUNT<="1111110"; when "0001"=>C_OUNT<="0000110"; when "0010"=>C_OUNT<="1101101"; when "0011"=>C_OUNT<="1111001"; when "0100"=>C_OUNT<="0110011"; when "0101"=>C_OUNT<="1011011"; when "0110"=>C_OUNT<="1011111"; when "0111"=>C_OUNT<="1110000"; when "1000"=>C_OUNT<="1111111"; when "1001"=>C_OUNT<="1111011"; when others=>C_OUNT<="0000000"; end case; cnt:=cnt+1; ELSIF cnt=1 THEN SMG_WX<="00000010"; case C_IN2 is when "0000"=>C_OUNT<="1111110"; when "0001"=>C_OUNT<="0110000"; when "0010"=>C_OUNT<="1101101"; when "0011"=>C_OUNT<="1111001"; when "0100"=>C_OUNT<="0110011"; when "0101"=>C_OUNT<="1011011"; when "0110"=>C_OUNT<="1011111"; when others=>C_OUNT<="0000000"; end case; cnt:=cnt+1; ELSIF cnt=2 THEN SMG_WX<="00000100"; case C_IN3 is when "0000"=>C_OUNT<="1111110"; when "0001"=>C_OUNT<="0000110"; when "0010"=>C_OUNT<="1101101"; when "0011"=>C_OUNT<="1111001"; when "0100"=>C_OUNT<="0110011"; when "0101"=>C_OUNT<="1011011"; when "0110"=>C_OUNT<="1011111"; when "0111"=>C_OUNT<="1110000"; when "1000"=>C_OUNT<="1111111"; when "1001"=>C_OUNT<="1111011"; when others=>C_OUNT<="0000000"; end case; cnt:=cnt+1; ELSIF cnt=3 THEN SMG_WX<="00001000"; case C_IN4 is when "0000"=>C_OUNT<="1111110"; when "0001"=>C_OUNT<="0110000"; when "0010"=>C_OUNT<="1101101"; when "0011"=>C_OUNT<="1111001"; when "0100"=>C_OUNT<="0110011"; when "0101"=>C_OUNT<="1011011"; when "0110"=>C_OUNT<="1011111"; when others=>C_OUNT<="0000000"; END CASE; cnt:=0; END IF; END IF; END PROCESS; END rtl;
最后的输入输出总线连接:
最后将讲到的四个主要器件、VCC、GND、数码管对应的引脚(并成总线形式),正确完整连接就组成了万年历:
但从这个设计我们发现,这个万年历并不完整!
从左往右看:只存在秒的个位、秒的十位、分的个位、分的十位,不存在小时!
编译下载效果如下:
这里的位选有些问题,而且我还没进编写空开 分与秒 位置的冒号显示:
回忆与感悟 :
这样接近底层对CLK时钟信号进行自由编程的感受释放奇妙,让我感觉任何信号的运转并不是凭空而来的,而是我自己靠着抽丝引线一点一点连接起来的!
现在许多STM32的编程都是调用库函数,导致很多人连寄存器都摸不到
而我从51、CPLD、汇编、硬件描述语言开始缓慢往上爬,到后来学习MSP432库函数残缺不完整,自己用库函数+寄存器混编,到STM32,操作系统,到现在学习树莓派、上位机
深刻感受到,编程的上限永远在底层,就像皇帝智力国家一样,只接触到大臣(调用库),只会在后面被底层人架空,然后终有一天被底层错误搅合得摸不着头脑。
如果底层都是直接可以被我摸清调遣的,那整个系统的逻辑就基本没有不明朗的地方了,出错也能比其余人更快找到病灶!