**这是本文档旧的修订版!**

STEP FPGA驱动无源蜂鸣器模块

本节将和大家一起使用FPGA驱动底板上的无源蜂鸣器模块实现不同音节的输出。

====硬件说明====

蜂鸣器的分类:

按其结构主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型:

  • 电磁式蜂鸣器由振荡器、电磁线圈、磁铁、振动膜片及外壳等组成。接通电源后,振荡器产生的音频信号电流通过电磁线圈,使电磁线圈产生磁场,振动膜片在电磁线圈和磁铁的相互作用下,周期性地振动发声。
  • 压电式蜂鸣器主要由多谐振荡器、压电蜂鸣片、阻抗匹配器及共鸣箱、外壳等组成。多谐振荡器由晶体管或集成电路构成,当接通电源后(1.5~15V直流工作电压),多谐振荡器起振,输出1.5~2.5kHZ的音频信号,阻抗匹配器推动压电蜂鸣片发声。

按是否带有信号源分为有源蜂鸣器和无源蜂鸣器两种类型:

  • 有源蜂鸣器只需要在其供电端加上额定直流电压,其内部的震荡器就可以产生固定频率的信号,驱动蜂鸣器发出声音。
  • 无源蜂鸣器可以理解成与喇叭一样,需要在其供电端上加上高低不断变化的电信号才可以驱动发出声音。


本章节和大家一起学习无源蜂鸣器的驱动,FPGA或单片机的GPIO口驱动能力弱,不能直接驱动无源蜂鸣器,常用的蜂鸣器驱动电路如下:

蜂鸣器使用NPN三极管(9013)驱动,三极管当开关用,当基极电压拉高时,蜂鸣器通电,当基极电压拉低时,蜂鸣器断电,FPGA控制GPIO口给三极管的基极输出不同频率的脉冲信号,蜂鸣器就可以发出不同的音节。
不同音节与蜂鸣器震荡频率的对应关系如下:

我们使用PWM的方法(关于PWM的说明,快速入门中的脉冲发生器章节有详细的介绍),使用计数器对系统时钟进行分频,改变计数器的计数终值从而实现调节PWM信号频率的目的,使用PWM信号控制蜂鸣器电路。

====Verilog代码====

// --------------------------------------------------------------------
// >>>>>>>>>>>>>>>>>>>>>>>>> COPYRIGHT NOTICE <<<<<<<<<<<<<<<<<<<<<<<<<
// --------------------------------------------------------------------
// Module:Segment_scan 
// 
// Author: Step
// 
// Description: Display with Segment tube
// 
// Web: www.stepfpga.com
// 
// --------------------------------------------------------------------
// Code Revision History :
// --------------------------------------------------------------------
// Version: |Mod. Date:   |Changes Made:
// V1.0     |2015/11/11   |Initial ver
// --------------------------------------------------------------------
module Segment_scan
(
input				clk_in,			//系统时钟
input				rst_n_in,		//系统复位,低有效
input		[3:0]	seg_data_1,		//SEG1 数码管要显示的数据
input		[3:0]	seg_data_2,		//SEG2 数码管要显示的数据
input		[3:0]	seg_data_3,		//SEG3 数码管要显示的数据
input		[3:0]	seg_data_4,		//SEG4 数码管要显示的数据
input		[3:0]	seg_data_5,		//SEG5 数码管要显示的数据
input		[3:0]	seg_data_6,		//SEG6 数码管要显示的数据
input		[5:0]	seg_data_en,	//各位数码管数据显示使能,[MSB~LSB]=[SEG6~SEG1]
input		[5:0]	seg_dot_en,		//各位数码管小数点显示使能,[MSB~LSB]=[SEG6~SEG1]
output	reg			rclk_out,		//74HC595的RCK管脚
output	reg			sclk_out,		//74HC595的SCK管脚
output	reg			sdio_out		//74HC595的SER管脚
);
 
parameter CLK_DIV_PERIOD = 600; //分频系数
 
localparam	IDLE	=	3'd0;
localparam	MAIN	=	3'd1;
localparam	WRITE	=	3'd2;
 
localparam	LOW		=	1'b0;
localparam	HIGH	=	1'b1;
 
//创建数码管的字库,字库数据依段码顺序有关
//这里字库数据[MSB~LSB]={DP,G,F,E,D,C,B,A}
reg[7:0] seg [15:0]; 
initial begin
    seg[0]	=	8'h3f;   //  0
    seg[1]	=	8'h06;   //  1
    seg[2]	=	8'h5b;   //  2
    seg[3]	=	8'h4f;   //  3
    seg[4]	=	8'h66;   //  4
    seg[5]	=	8'h6d;   //  5
    seg[6]	=	8'h7d;   //  6
    seg[7]	=	8'h07;   //  7
    seg[8]	=	8'h7f;   //  8
    seg[9]	=	8'h6f;   //  9
	seg[10]	=	8'h77;   //  A
    seg[11]	=	8'h7c;   //  b
    seg[12]	=	8'h39;   //  C
    seg[13]	=	8'h5e;   //  d
    seg[14]	=	8'h79;   //  E
    seg[15]	=	8'h71;   //  F
end 
 
//计数器对系统时钟信号进行计数
reg[9:0] cnt=0;
always@(posedge clk_in or negedge rst_n_in) begin
	if(!rst_n_in) begin
		cnt <= 1'b0;
	end else begin
		if(cnt>=(CLK_DIV_PERIOD-1)) cnt <= 1'b0;
		else cnt <= cnt + 1'b1;
	end
end
 
//根据计数器计数的周期产生分频的脉冲信号
reg clk_div; 
always@(posedge clk_in or negedge rst_n_in) begin
	if(!rst_n_in) begin
		clk_div <= 1'b0;
	end else begin
		if(cnt==(CLK_DIV_PERIOD-1)) clk_div <= 1'b1;
		else clk_div <= 1'b0;
	end
end
 
//使用状态机完成数码管的扫描和74HC595时序的实现
reg		[15:0]		data_reg;
reg		[2:0]		cnt_main;
reg		[5:0]		cnt_write;
reg		[2:0] 		state = IDLE;
always@(posedge clk_in or negedge rst_n_in) begin
	if(!rst_n_in) begin	//复位状态下,各寄存器置初值
		state <= IDLE;
		cnt_main <= 3'd0;
		cnt_write <= 6'd0;
		sdio_out <= 1'b0;
		sclk_out <= LOW;
		rclk_out <= LOW;
	end else begin
		case(state)
			IDLE:begin	//IDLE作为第一个状态,相当于软复位
					state <= MAIN;
					cnt_main <= 3'd0;
					cnt_write <= 6'd0;
					sdio_out <= 1'b0;
					sclk_out <= LOW;
					rclk_out <= LOW;
				end
			MAIN:begin
					if(cnt_main >= 3'd5) cnt_main <= 1'b0;
					else cnt_main <= cnt_main + 1'b1;
					case(cnt_main)
						//对6位数码管逐位扫描
						3'd0:	begin 
									state <= WRITE;	//在配置完发给74HC595的数据同时跳转至WRITE状态,完成串行时序
									data_reg <= {seg[seg_data_1]|(seg_dot_en[0]?8'h80:8'h00),seg_data_en[0]?8'hfe:8'hff};
									//data_reg[15:8]为段选,data_reg[7:0]为位选
									//seg[seg_data_1]  是根据端口的输入获取相应字库数据
									//seg_dot_en[0]?8'h80:8'h00  是根据小数点显示使能信号 控制SEG1数码管的小数点DP段的电平
									//seg_data_en[0]?8'hfe:8'hff  是根据数据显示使能信号 控制SEG1数码管的位选引脚的电平
								end
						3'd1:	begin 
									state <= WRITE;
									data_reg <= {seg[seg_data_2]|(seg_dot_en[1]?8'h80:8'h00),seg_data_en[1]?8'hfd:8'hff}; 
								end
						3'd2:	begin 
									state <= WRITE;
									data_reg <= {seg[seg_data_3]|(seg_dot_en[2]?8'h80:8'h00),seg_data_en[2]?8'hfb:8'hff}; 
								end
						3'd3:	begin 
									state <= WRITE;
									data_reg <= {seg[seg_data_4]|(seg_dot_en[3]?8'h80:8'h00),seg_data_en[3]?8'hf7:8'hff}; 
								end
						3'd4:	begin 
									state <= WRITE;
									data_reg <= {seg[seg_data_5]|(seg_dot_en[4]?8'h80:8'h00),seg_data_en[4]?8'hef:8'hff};
								end
						3'd5:	begin 
									state <= WRITE;
									data_reg <= {seg[seg_data_6]|(seg_dot_en[5]?8'h80:8'h00),seg_data_en[5]?8'hdf:8'hff}; 
								end
						default: state <= IDLE;
					endcase
				end
			WRITE:begin
					if(clk_div) begin	//74HC595的串行时钟有速度要求,需要按照分频后的节拍
						if(cnt_write >= 6'd33) cnt_write <= 1'b0;
						else cnt_write <= cnt_write + 1'b1;
						case(cnt_write)
							//74HC595是串行转并行的芯片,3路输入可产生8路输出,而且可以级联使用
							//74HC595的时序实现,参考74HC595的芯片手册
							6'd0:  begin sclk_out <= LOW; sdio_out <= data_reg[15]; end		//SCK下降沿时SER更新数据
							6'd1:  begin sclk_out <= HIGH; end								//SCK上升沿时SER数据稳定
							6'd2:  begin sclk_out <= LOW; sdio_out <= data_reg[14]; end
							6'd3:  begin sclk_out <= HIGH; end
							6'd4:  begin sclk_out <= LOW; sdio_out <= data_reg[13]; end
							6'd5:  begin sclk_out <= HIGH; end
							6'd6:  begin sclk_out <= LOW; sdio_out <= data_reg[12]; end
							6'd7:  begin sclk_out <= HIGH; end
							6'd8:  begin sclk_out <= LOW; sdio_out <= data_reg[11]; end
							6'd9:  begin sclk_out <= HIGH; end
							6'd10: begin sclk_out <= LOW; sdio_out <= data_reg[10]; end
							6'd11: begin sclk_out <= HIGH; end
							6'd12: begin sclk_out <= LOW; sdio_out <= data_reg[9]; end
							6'd13: begin sclk_out <= HIGH; end
							6'd14: begin sclk_out <= LOW; sdio_out <= data_reg[8]; end
							6'd15: begin sclk_out <= HIGH; end
							6'd16: begin sclk_out <= LOW; sdio_out <= data_reg[7]; end
							6'd17: begin sclk_out <= HIGH; end
							6'd18: begin sclk_out <= LOW; sdio_out <= data_reg[6]; end
							6'd19: begin sclk_out <= HIGH; end
							6'd20: begin sclk_out <= LOW; sdio_out <= data_reg[5]; end
							6'd21: begin sclk_out <= HIGH; end
							6'd22: begin sclk_out <= LOW; sdio_out <= data_reg[4]; end
							6'd23: begin sclk_out <= HIGH; end
							6'd24: begin sclk_out <= LOW; sdio_out <= data_reg[3]; end
							6'd25: begin sclk_out <= HIGH; end
							6'd26: begin sclk_out <= LOW; sdio_out <= data_reg[2]; end
							6'd27: begin sclk_out <= HIGH; end
							6'd28: begin sclk_out <= LOW; sdio_out <= data_reg[1]; end
							6'd29: begin sclk_out <= HIGH; end
							6'd30: begin sclk_out <= LOW; sdio_out <= data_reg[0]; end
							6'd31: begin sclk_out <= HIGH; end
							6'd32: begin rclk_out <= HIGH; end								//当16位数据传送完成后RCK拉高,输出生效
							6'd33: begin rclk_out <= LOW; state <= MAIN; end
							default: state <= IDLE;
						endcase
					end else begin
						sclk_out <= sclk_out;
						sdio_out <= sdio_out;
						rclk_out <= rclk_out;
						cnt_write <= cnt_write;
						state <= state;
					end
				end
			default: state <= IDLE;
		endcase
	end
end
 
endmodule



====小结====

本节主要为大家讲解了数码管显示的相关原理及软件设计,需要大家掌握的同时自己创建工程,通过整个设计流程,生成FPGA配置文件加载测试。
如果你对Diamond软件的使用不了解,请参考这里:Diamond的使用

====相关资料====


使用STEP-MXO2第二代的数码管扫描程序: 后续会有下载连接 待更新
使用STEP-MAX10的数码管扫描程序: 后续会有下载连接 待更新