转载

语音通信解决方案总结

语音通信方案

系统级方案和自建协议

windows平台、linux平台、嵌入式linux平台、mcu平台

1,在嵌入式Linux上开发的有线通信语音解决方案

这方案是在嵌入式Linux上开发的,音频方案基于ALSA,语音通信相关的都是在user space 做,算是一个上层的解决方案。由于是有线通信,网络环境相对无线通信而言不是特别恶劣,用的丢包补偿措施也不是很多,主要有PLC、RFC2198等。

2,在Android手机上开发的传统无线通信语音解决方案

这方案是在Android手机上开发的,是手机上的传统语音通信方案(相对于APP语音通信而言)。Android是基于Linux的,所以也会用到ALSA,但是主要是做控制用,如对codec芯片的配置等。跟音频数据相关的驱动、编解码、前后处理等在Audio DSP上开发,网络侧相关的在CP(通信处理器)上开发,算是一个底层解决方案。该方案的软件框图如下:

系统级

声卡(Sound Card)也叫 音频 卡(港台称之为声效卡),是计算机多媒体系统中最基本的组成部分,是实现 声波 / 数字信号 相互转换的一种 硬件 。声卡的基本功能是把来自 话筒 、磁带、光盘的原始声音 信号 加以转换,输出到 耳机 、 扬声器 、 扩音机 、 录音机 等声响 设备 ,或通过 音乐 设备数字接口( MIDI )发出合成乐器的声音

所有的电脑主板基本都有集成声卡的,如果有专业要求会再买个独立声卡,就像专业玩家一样买个独立显卡,手动狗头

声卡驱动

对于音频处理的技术,主要有如下几种:

  • 采集麦克风输入
  • 采集声卡输出
  • 将音频数据送入声卡进行播放
  • 对多路音频输入进行混音处理

Windows平台内核提供调用声卡API

一、MME(MultiMedia Extensions)

MME就是winmm.dll提供的接口,也是Windows平台下第一代API。优点是使用简单,一般场景下可以满足业务需求,缺点是延迟高,某些高级功能无法实现。

二、 XAudio2

也是DirextX的一部分,为了取代DirectSound。DirextX套件中的音频组件,大多用于游戏中,支持硬件加速,所以比MME有更低的延迟。

三、 Core Audio API

Vista系统开始引入的新架构,它是以COM的方式提供的接口,用户模式下处于最底层,上面提到的几种API最终都将使用它!功能最强,性能最好,但是接口繁杂,使用起来很麻烦。

四、 Wasapi 就可以了 (高性能,但更复杂)

而Wave系列的API函数主要是用来实现对麦克风输入的采集(使用WaveIn系列API函数)和控制声音的播放(使用后WaveOut系列函数)。

1.使用WaveIn系列API函数实现麦克风输入采集

涉及的API函数:

  • waveInOpen

    开启音频采集设备,成功后会返回设备句柄,后续的API都需要使用该句柄

    调用模块需要提供一个回调函数(waveInProc),以接收采集的音频数据

  • waveInClose

    关闭音频采集模块

    成功后,由waveInOpen返回的设备句柄将不再有效 

  • waveInPrepareHeader

    准备音频采集数据缓存的空间

  • waveInUnprepareHeader

    清空音频采集的数据缓存

  • waveInAddBuffer

    将准备好的音频数据缓存提供给音频采集设备

    在调用该API之前需要先调用waveInPrepareHeader

  • waveInStart

    控制音频采集设备开始对音频数据的采集

  • waveInStop

    控制音频采集设备停止对音频数据的采集

音频采集设备采集到音频数据后,会调用在waveInOpen中设置的回调函数。

其中参数包括一个消息类型,根据其消息类型就可以进行相应的操作。

如接收到WIM_DATA消息,则说明有新的音频数据被采集到,这样就可以根据需要来对这些音频数据进行处理。

(示例以后补上)

2.使用Core Audio实现对声卡输出的捕捉

涉及的接口有:

  • IMMDeviceEnumerator

  • IMMDevice

  • IAudioClient

  • IAudioCaptureClient

主要过程:

  • 创建多媒体设备枚举器(IMMDeviceEnumerator)

  • 通过多媒体设备枚举器获取声卡接口(IMMDevice)

  • 通过声卡接口获取声卡客户端接口(IAudioClient)

  • 通过声卡客户端接口(IAudioClient)可获取声卡输出的音频参数、初始化声卡、获取声卡输出缓冲区的大小、开启/停止对声卡输出的采集

  • 通过声卡采集客户端接口(IAudioCaptureClient)可获取采集的声卡输出数据,并对内部缓冲区进行控制

(示例以后补上)

3.常用的混音算法

混音算法就是将多路音频输入信号根据某种规则进行运算(多路音频信号相加后做限幅处理),得到一路混合后的音频,并以此作为输出的过程。

我目前还做过这一块,搜索了一下基本有如下几种混音算法:

  • 将多路音频输入信号直接相加取和作为输出

  • 将多路音频输入信号直接相加取和后,再除以混音通道数,防止溢出

  • 将多路音频输入信号直接相加取和后,做Clip操作(将数据限定在最大值和最小值之间),如有溢出就设最大值

  • 将多路音频输入信号直接相加取和后,做饱和处理,接近最大值时进行扭曲

  • 将多路音频输入信号直接相加取和后,做归一化处理,全部乘个系数,使幅值归一化

  • 将多路音频输入信号直接相加取和后,使用衰减因子限制幅值

Linux平台内核提供调用声卡API

ALSA是目前linux的主流音频体系架构

是一个有社区维护的开源项目:http://www.alsa-project.org/

包括:

1.内核驱动包 alsa-driver

2.用户空间库 alsa-lib

3.附加库插件包 alsa-libplugins

4.音频处理工具集 alsa-utils

5.其他音频处理小工具包 alsa-tools

6.特殊音频固件支持包 alsa-firmware

7.alsa-lib的Python绑定包 pyalsa

8.OSS接口兼容包 alsa-oss

9.内核空间中,alsa-soc其实是对alsa-driver的进一步封装,他针对嵌入式设备提供了一些列增强的功能。

1.操作说明

安装

sudo apt install libasound2-dev

流程

  • 打开设备
  • 分配参数内存
  • 填充默认参数
  • 设置参数(详细的参见  ALSA - PCM接口 )
    • 通道数
    • 采样率(码率,用来指定时间和文件大小,frames/s)
    • 帧数(每次读取的数据长度与该参数有关)
    • 数据格式(影响输出数据、缓存大小)
    • 设备访问类型(直接读写、内存映射,交错模式、非交错模式)
  • 读取、写入数据

简单的例子

包含头文件

#include <alsa/asoundlib.h>

查看设备,根据最后两个数字确定设备名称,通常default就行了

aplay -L

定义相关参数,录放音都要经过相同的步骤,放一起定义

// 设备名称,这里采用默认,还可以选取"hw:0,0","plughw:0,0"等
const char *device = "default";
// 设备句柄
// 以下均定义两个,根据前缀区分,c->capture,p->playback,没有前缀的表示参数相同
snd_pcm_t *chandle;
snd_pcm_t *phandle;
// 硬件参数
snd_pcm_hw_params_t *cparams;
snd_pcm_hw_params_t *pparams;
// 数据访问类型,读写方式:内存映射或者读写,数据
snd_pcm_access_t access_type = SND_PCM_ACCESS_RW_INTERLEAVED;
// 格式,
snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE;
// 码率,采样率,8000Hz,44100Hz
unsigned int rate = 44100;
// 通道数
unsigned int channels = 2;
// 帧数,这里取32
snd_pcm_uframes_t frames = 32;
// 以下为可选参数
unsigned int bytes_per_frame;
// 软件重采样
unsigned int soft_resample;

打开设备

snd_pcm_open(&chandle, device, SND_PCM_STREAM_CAPTURE, 0);
snd_pcm_open(&phandle, device, SND_PCM_STREAM_PLAYBACK, 0);

增加一个错误判断

int err;
if ((err = snd_pcm_open(&chandle, device, SND_PCM_STREAM_CAPTURE, 0)) < 0)
{
    std::cout << "Capture device open failed.";
}
if ((err = snd_pcm_open(&phandle, device, SND_PCM_STREAM_PLAYBACK, 0)) < 0)
{
    std::cout << "Playback device open failed.";
}

设置参数,这里就不增加错误判断了,不然显得有些长了

// 先计算每帧数据的大小
bytes_per_frame = snd_pcm_format_width(format) / 8 * 2;
// 计算需要分配的缓存空间的大小
buffer_size = frames * bytes_per_frame;

// 为参数分配空间
snd_pcm_hw_params_alloca(&params);
// 填充参数空间
snd_pcm_hw_params_any(handle, params);
// 设置数据访问方式
snd_pcm_hw_params_set_access(handle, params, access_type);
// 设置格式
snd_pcm_hw_params_set_format(handle, params, format);
// 设置通道
snd_pcm_hw_params_set_channels(handle, params, channels);
// 设置采样率
snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0);

// 可选项,不改不影响
// 设置缓存大小
buffer_size = period_size * 2;
snd_pcm_hw_params_set_buffer_size_near(handle, params, &buffer_size);
// 设置段大小,period与OSS中的segment类似
period_size = buffer_size / 2;
snd_pcm_hw_params_set_period_size_near(handle, params, ._size, 0));

//设置参数
snd_pcm_hw_params(handle, params);

读写数据

// 分配缓存空间,大小上面通过buffer_size计算出了
char *buffer = (char *)malloc(buffer_size);
// 读写数据
snd_pcm_readi(chandle, buffer, frames);
snd_pcm_writei(phandle, buffer, frames);

循环播放

while(1)
{
    snd_pcm_readi(chandle, buffer, frames);
    snd_pcm_writei(phandle, buffer, frames);
}

捕获一定时间的音频数据到文件流

ofstream output("test.pcm", ios::trunc);

int loop_sec;
int frames_readed;
loop_sec = 10;
unsigned long loop_limit;
// 计算循环大小
loop_limit = loop_sec * rate;

for (size_t i = 0; i < loop_limit; )
{
    // 这里还需要判断一下返回值是否为负
    frames_readed = snd_pcm_readi(chandle, buffer, frames);
    output.write(buffer, buffer_size);
    i += frames_readed;
}

关闭设备、释放指针

snd_pcm_close(chandle);
snd_pcm_close(phandle);
free(buffer);

放音过程中也许会出现"Broken pipe"的错误,添加如下需要重新准备设备

err = snd_pcm_writei(handle, input_buffer, frames);
if (err == -EPIPE)
{
    snd_pcm_prepare(handle);
    continue;
    // 或者
    // return 0;
}

完整例子


 1 #ifndef ALSA_AUDIO_H
 2 #define ALSA_AUDIO_H
 3 
 4 #include <QObject>
 5 
 6 #include <alsa/asoundlib.h>
 7 
 8 class ALSA_Audio : public QObject
 9 {
10     Q_OBJECT
11 public:
12     explicit ALSA_Audio(QObject *parent = nullptr);
13 
14 
15     void capture_start();
16     void capture_stop();
17     /**
18      * @brief 读取音频数据
19      * @param buffer 音频数据
20      * @param buffer_size 音频数据大小
21      * @param frames 读取的音频帧数
22      * @return 0 成功,-1 失败
23      */
24     int audio_read(char **buffer, int *buffer_size, unsigned long *frames);
25 
26     void playback_start();
27     void playback_stop();
28     /**
29      * @brief audio_write 播放音频
30      * @param buffer 音频数据
31      * @param frames 播放的音频帧数
32      * @return 0 成功,-1 失败
33      */
34     int audio_write(char *buffer);
35 
36 
37 
38 private:
39     bool m_is_capture_start;
40     snd_pcm_t *m_capture_pcm;
41     char *m_capture_buffer;
42     unsigned long m_capture_buffer_size;
43     snd_pcm_uframes_t m_capture_frames;       // 一次读的帧数
44 
45 
46     bool m_is_playback_start;
47     snd_pcm_t *m_playback_pcm;
48     snd_pcm_uframes_t m_playback_frames;       // 一次写的帧数
49 
50     /**
51      * @brief ALSA_Audio::set_hw_params
52      * @param pcm
53      * @param hw_params
54      * @param rate 采样频率
55      * @param format 格式
56      * @param channels 通道数
57      * @param frames 一次读写的帧数
58      * @return
59      */
60     int set_hw_params(snd_pcm_t *pcm, unsigned int rate, snd_pcm_format_t format, unsigned int channels, snd_pcm_uframes_t frames);
61 
62 
63 
64 signals:
65 
66 public slots:
67 };
68 
69 #endif // ALSA_AUDIO_H

alsa_audio.h

  1 #include "alsa_audio.h"
  2 #include "global.h"
  3 
  4 #include <QDebug>
  5 
  6 #include <math.h>
  7 #include <inttypes.h>
  8 
  9 
 10 
 11 ALSA_Audio::ALSA_Audio(QObject *parent) : QObject(parent)
 12 {
 13     m_is_capture_start = false;
 14     m_is_playback_start = false;
 15 }
 16 
 17 
 18 
 19 int ALSA_Audio::set_hw_params(snd_pcm_t *pcm, unsigned int rate, snd_pcm_format_t format, unsigned int channels, snd_pcm_uframes_t frames)
 20 {
 21     snd_pcm_uframes_t period_size;          // 一个处理周期需要的帧数
 22     snd_pcm_uframes_t hw_buffer_size;      // 硬件缓冲区大小
 23     snd_pcm_hw_params_t *hw_params;
 24     int ret;
 25     int dir = 0;
 26 
 27 
 28 
 29     // 初始化硬件参数结构体
 30     snd_pcm_hw_params_malloc(&hw_params);
 31     // 设置默认的硬件参数
 32     snd_pcm_hw_params_any(pcm, hw_params);
 33 
 34     // 以下为设置所需的硬件参数
 35 
 36     // 设置音频数据记录方式
 37     CHECK_RETURN(snd_pcm_hw_params_set_access(pcm, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED));
 38     // 格式。使用16位采样大小,小端模式(SND_PCM_FORMAT_S16_LE)
 39     CHECK_RETURN(snd_pcm_hw_params_set_format(pcm, hw_params, format));
 40     // 设置音频通道数
 41     CHECK_RETURN(snd_pcm_hw_params_set_channels(pcm, hw_params, channels));
 42     // 采样频率,一次采集为一帧数据
 43     //CHECK_RETURN(snd_pcm_hw_params_set_rate_near(pcm, hw_params, &rate, &dir));          // 设置相近的值
 44     CHECK_RETURN(snd_pcm_hw_params_set_rate(pcm, hw_params, rate, dir));
 45     // 一个处理周期需要的帧数
 46     period_size = frames * 5;
 47     CHECK_RETURN(snd_pcm_hw_params_set_period_size_near(pcm, hw_params, ._size, &dir)); // 设置相近的值
 48 //    // 硬件缓冲区大小, 单位:帧(frame)
 49 //    hw_buffer_size = period_size * 16;
 50 //    CHECK_RETURN(snd_pcm_hw_params_set_buffer_size_near(pcm, hw_params, &hw_buffer_size));
 51 
 52     // 将参数写入pcm驱动
 53     CHECK_RETURN(snd_pcm_hw_params(pcm, hw_params));
 54 
 55     snd_pcm_hw_params_free(hw_params);     // 释放不再使用的hw_params空间
 56 
 57     printf("one frames=%ldbytes/n", snd_pcm_frames_to_bytes(pcm, 1));
 58     unsigned int val;
 59     snd_pcm_hw_params_get_channels(hw_params, &val);
 60     printf("channels=%d/n", val);
 61 
 62     if (ret < 0) {
 63         printf("error: unable to set hw parameters: %s/n", snd_strerror(ret));
 64         return -1;
 65     }
 66     return 0;
 67 }
 68 
 69 
 70 void ALSA_Audio::capture_start()
 71 {
 72     m_capture_frames = 160;     // 此处160为固定值,发送接收均使用此值
 73     unsigned int rate = 8000;                               // 采样频率
 74     snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE;        // 使用16位采样大小,小端模式
 75     unsigned int channels = 1;                              // 通道数
 76     int ret;
 77 
 78     if(m_is_capture_start)
 79     {
 80         printf("error: alsa audio capture is started!/n");
 81         return;
 82     }
 83 
 84     ret = snd_pcm_open(&m_capture_pcm, "plughw:1,0", SND_PCM_STREAM_CAPTURE, 0);       // 使用plughw:0,0
 85     if(ret < 0)
 86     {
 87         printf("snd_pcm_open error: %s/n", snd_strerror(ret));
 88         return;
 89     }
 90 
 91     // 设置硬件参数
 92     if(set_hw_params(m_capture_pcm, rate, format, channels, m_capture_frames) < 0)
 93     {
 94         return;
 95     }
 96 
 97     // 使用buffer保存一次处理得到的数据
 98     m_capture_buffer_size = m_capture_frames * static_cast<unsigned long>(snd_pcm_format_width(format) / 8 * static_cast<int>(channels));
 99     m_capture_buffer_size *= 5;         // * 5 表示使用5倍的缓存空间
100     printf("snd_pcm_format_width(format):%d/n", snd_pcm_format_width(format));
101     printf("m_capture_buffer_size:%ld/n", m_capture_buffer_size);
102     m_capture_buffer = static_cast<char *>(malloc(sizeof(char) * m_capture_buffer_size));
103     memset(m_capture_buffer, 0, m_capture_buffer_size);
104 
105     // 获取一次处理所需要的时间,单位us
106     // 1/rate * frames * 10^6 = period_time, 即:采集一帧所需的时间 * 一次处理所需的帧数 * 10^6 = 一次处理所需的时间(单位us)
107     // snd_pcm_hw_params_get_period_time(m_capture_hw_params, &m_period_time, &dir);
108 
109     m_is_capture_start = true;
110 }
111 
112 void ALSA_Audio::capture_stop()
113 {
114     if(m_is_capture_start == false)
115     {
116         printf("error: alsa audio capture is not start!");
117         return;
118     }
119 
120     m_is_capture_start = false;
121 
122     snd_pcm_drain(m_capture_pcm);
123     snd_pcm_close(m_capture_pcm);
124     free(m_capture_buffer);
125 }
126 
127 int ALSA_Audio::audio_read(char **buffer, int *buffer_size, unsigned long *frames)
128 {
129     int ret;
130     if(m_is_capture_start == false)
131     {
132         printf("error: alsa audio capture is stopped!/n");
133         return -1;
134     }
135     memset(m_capture_buffer, 0, m_capture_buffer_size);
136     ret = static_cast<int>(snd_pcm_readi(m_capture_pcm, m_capture_buffer, m_capture_frames));
137     printf("strlen(m_capture_buffer)=%ld/n", strlen(m_capture_buffer));
138     if (ret == -EPIPE)
139     {
140        /* EPIPE means overrun */
141        printf("overrun occurred/n");
142        snd_pcm_prepare(m_capture_pcm);
143     }
144     else if (ret < 0)
145     {
146        printf("error from read: %s/n", snd_strerror(ret));
147     }
148     else if (ret != static_cast<int>(m_capture_frames))
149     {
150        printf("short read, read %d frames/n", ret);
151     }
152 
153     if(m_capture_buffer == nullptr)
154     {
155         printf("error: alsa audio capture_buffer is empty!/n");
156         return -1;
157     }
158     *buffer = m_capture_buffer;
159     *buffer_size = static_cast<int>(m_capture_buffer_size / 5);
160     *frames = m_capture_frames;
161 
162     return 0;
163 }
164 
165 
166 
167 void ALSA_Audio::playback_start()
168 {
169     m_playback_frames = 160;     // 此处160为固定值,发送接收均使用此值
170     unsigned int rate = 8000;                               // 采样频率
171     snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE;        // 使用16位采样大小,小端模式
172     unsigned int channels = 1;                              // 通道数
173     int ret;
174 
175 
176     if(m_is_playback_start)
177     {
178         printf("error: alsa audio playback is started!/n");
179         return;
180     }
181 
182     ret = snd_pcm_open(&m_playback_pcm, "plughw:1,0", SND_PCM_STREAM_PLAYBACK, 0);      // 使用plughw:0,0
183     if(ret < 0)
184     {
185         printf("snd_pcm_open error: %s/n", snd_strerror(ret));
186         return;
187     }
188 
189     // 设置硬件参数
190     if(set_hw_params(m_playback_pcm, rate, format, channels, m_playback_frames) < 0)
191     {
192         return;
193     }
194 
195 
196     m_is_playback_start = true;
197 
198 }
199 
200 void ALSA_Audio::playback_stop()
201 {
202     if(m_is_playback_start == false)
203     {
204         printf("error: alsa audio playback is not start!");
205         return;
206     }
207 
208     m_is_playback_start = false;
209 
210     snd_pcm_drain(m_playback_pcm);
211     snd_pcm_close(m_playback_pcm);
212 }
213 
214 
215 int ALSA_Audio::audio_write(char *buffer)
216 {
217     long ret;
218     if(m_is_playback_start == false)
219     {
220         printf("error: alsa audio playback is stopped!/n");
221         return -1;
222     }
223     else
224     {
225         ret = snd_pcm_writei(m_playback_pcm, buffer, m_playback_frames);
226         if(ret == -EPIPE)
227         {
228             /* EPIPE means underrun  */
229             printf("underrun occurred/n");
230             snd_pcm_prepare(m_playback_pcm);
231         }
232         else if (ret < 0)
233         {
234            printf("error from write: %s/n", snd_strerror(static_cast<int>(ret)));
235         }
236         else if (ret != static_cast<long>(m_playback_frames))
237         {
238            printf("short write, write %ld frames/n", ret);
239         }
240     }
241     return 0;
242 }

alsa_audio.cpp

2.架构图

硬件架构:

语音通信解决方案总结

软件架构:

语音通信解决方案总结

3.初识alsa设备

语音通信解决方案总结

注:

controlC0:控制接口,用于控制声卡,如通道选择,混音,麦克风输入增益调节等。

midiC0D0:Raw迷笛接口,用于播放midi音频。

pcmC0D0c:pcm接口,用于录音的pcm设备。

pcmC0D0p:用于播放的pcm设备。

pcmC0D1p:

seq:音序器接口。

timer:定时器接口。

即该声卡下挂载了7个设备。根据声卡实际能力,驱动实际上可以挂载更多种类的设备

其中

C0D0表示声卡0中的设备0。

pcmC0D0c:最后的c表示capture。

pcmC0D0p:最后一个p表示playback。

设备种类 include/sound/core.h:

include/sound/core.h

4.linux内核中音频驱动代码分布

语音通信解决方案总结

其中:

core:包含 ALSA 驱动的核心层代码实现。

core/oss:包含模拟旧的OSS架构的PCM和Mixer模块。

core/seq:音序器相关的代码。

drivers:存放一些与CPU,bus架构无关的公用代码。

i2c:ALSA的i2c控制代码。

pci:PCI总线 声卡的顶层目录,其子目录包含各种PCI声卡代码。

isa:ISA总线 声卡的顶层目录,其子目录包含各种ISA声卡代码。

soc:ASoC(ALSA System on Chip)层实现代码,针对嵌入式音频设备。

soc/codecs:针对ASoC体系的各种音频编码器的驱动实现,与平台无关。

include/sound:ALSA驱动的公共头文件目录。

5.驱动分类

OSS音频设备驱动:

OSS 标准中有两个最基本的音频设备: mixer(混音器)和 dsp(数字信号处理器)。

ALSA音频设备驱动:

虽然 OSS 已经非常成熟,但它毕竟是一个没有完全开放源代码的商业产品,而且目前基本上在 Linux mainline 中失去了更新。而 ALSA (Advanced Linux Sound Architecture)恰好弥补了这一空白,它符合 GPL,是在 Linux 下进行音频编程时另一种可供选择的声卡驱动体系结构。 ALSA 除了像 OSS 那样提供了一组内核驱动程序模块之外,还专门为简化应用程序的编写提供了相应的函数库,与 OSS 提供的基于 ioctl 的原始编程接口相比, ALSA 函数库使用起来要更加方便一些。 ALSA 的主要特点如下。支持多种声卡设备。

模块化的内核驱动程序。

支持 SMP 和多线程。

提供应用开发函数库(alsa-lib)以简化应用程序开发。

支持 OSS API,兼容 OSS 应用程序。

ASoC音频设备驱动:

ASoC(ALSA System on Chip)是 ALSA 在 SoC 方面的发展和演变,它在本质上仍然属于

ALSA,但是在 ALSA 架构基础上对 CPU 相关的代码和 Codec 相关的代码进行了分离。其原因是, 采用传统 ALSA 架构的情况下,同一型号的 Codec 工作于不同的 CPU 时,需要不同的驱动,这 不符合代码重用的要求。 对于目前嵌入式系统上的声卡驱动开发,我们建议读者尽量采用 ASoC 框架, ASoC 主要 由 3 部分组成。

Codec 驱动。这一部分只关心 Codec 本身,与 CPU 平台相关的特性不由此部分操作。

平台驱动 。这一部分只关心 CPU 本身,不关心 Codec。它主要处理两个问题: DMA 引 擎和 SoC 集成的 PCM、 I2S 或 AC ‘97 数字接口控制。

板驱动 (也称为 machine 驱动)。这一部分将平台驱动和 Codec 驱动绑定在一起,描述了 板一级的硬件特征。

在以上 3 部分中, 1 和 2 基本都可以仍然是通用的驱动了,也就是说, Codec 驱动认为自己 可以连接任意 CPU,而 CPU 的 I2S、 PCM 或 AC ‘97 接口对应的平台驱动则认为自己可以连接任 意符合其接口类型的 Codec,只有 3 是不通用的,由特定的电路板上具体的 CPU 和 Codec 确定, 因此它很像一个插座,上面插上了 Codec 和平台这两个插头。 在以上三部分之上的是 ASoC 核心层,由内核源代码中的 sound/soc/soc-core.c 实现,查看其 源代码发现它完全是一个传统的 ALSA 驱动。因此,对于基于 ASoC 架构的声卡驱动而言, alsa-lib 以及 ALSA 的一系列 utility 仍然是可用的,如 amixer、 aplay 均无需针对 ASoC 进行任何改动。而 ASoC 的用户编程方法也与 ALSA 完全一致。 内核源代码的 Documentation/sound/alsa/soc/目录包含了 ASoC 相关的文档。

Android平台内核提供调用声卡API

目前linux中主流的音频体系结构是ALSA(Advanced Linux Sound Architecture),ALSA在内核驱动层提供了alsa-driver,在应用层提供了alsa-lib,应用程序只需要调用alsa-lib(libtinyalsa.so)提供的API就可以完

成对底层硬件的操作。说的这么好,但是Android中没有使用标准的ALSA,而是一个ALSA的简化版叫做tinyalsa。Android中使用tinyalsa控制管理所有模式的音频通路,我们也可以使用tinyalsa提供的工具进行查看、

调试。

TINYALSA子系统

tinycap.c 实现录音相关代码  tinycap

Tinyplay.c 实现放音相关代码 tinyplay

Pcm.c 与驱动层alsa-driver调用接口,为audio_hw提供api接口

Tinymix 查看和设置混音器 tinymix

Tinypcminfo.c 查看声卡信息tinypcminfo

音频帧(frame)

这个概念在应用开发中非常重要,网上很多文章都没有专门介绍这个概念。

音频跟视频很不一样,视频每一帧就是一张图像,而从上面的正玄波可以看出,音频数据是流式的,本身没有明确的一帧帧的概念,在实际的应用中,为了音频算法处理/传输的方便,一般约定俗成取2.5ms~60ms为单位的数据量为一帧音频。

语音通信解决方案总结

这个时间被称之为“采样时间”,其长度没有特别的标准,它是根据编解码器和具体应用的需求来决定的,我们可以计算一下一帧音频帧的大小:

假设某音频信号是采样率为8kHz、双通道、位宽为16bit,20ms一帧,则一帧音频数据的大小为:

int size = 8000 x 2 x 16bit x 0.02s = 5120bit = 640 byte

音频帧总结

period(周期):硬件中断间的间隔时间。它表示输入延时。

声卡接口中有一个指针来指示声卡硬件缓存区中当前的读写位置。只要接口在运行,这个指针将循环地指向缓存区中的某个位置。

frame size =sizeof(one sample) * nChannels

alsa中配置的缓存(buffer)和周期(size)大小在runtime中是以帧(frames)形式存储的。

period_bytes =pcm_format_to_bits 用来计算一个帧有多少bits,实际应用的时候经常用到

嵌入式硬件级

电路组成

简单流程:

MIC采集自然声转成模拟电信号,通过运算放大电路放大信号幅度,然后用ADC转换为数字信号,(可以进行音频的编码工作,比如编成mp3),(然后进行音频解码工作),(通过DAC转换为模拟信号)(或者脉冲宽度调制PWM用来对模拟信号的电平进行数字编码),通过功率放大器放大后输出给喇叭

看什么方案了,如果涉及比较复杂的运算,MCU的算力是远远不够的,必须上嵌入式硬件了,这就涉及到系统层面的开发。如果只是简单的音频处理没事(比如MP3节奏彩灯录放等等)

其他方案:

1 利用语言集成芯片如:ISD2560,ISD2560采用多电平直接 模拟量 存储技术,可以非常真实、自然的再现语音、音乐、音调和效果声,录音时间为60s,可重复录放10万次。

2 PWM+SPI PWM模拟时钟时序,SPI传输数据,采用PCM编码方式,然后接放大器+喇叭;

(软件编写很简单,只把wave文件的采样值往pwm里面丢就可以了。当然,pwm信号一般需要加滤波电路才能送往功放、喇叭。 一般采用16kbps的采样率,滤波电路会简单。)

3 DAC DAC+放大器+喇叭,一般语音芯片都是用这种方式做的,但是应该是专用的DAC语音芯片;

4 IIS+语音解码芯片

这些总线协议什么I2C SPI等都是用来接外围集成电路的

其实所谓的音频编码器、解码器。实际上就是普通的AD或DA后,再由运算芯片进行算法压缩或解压来的

编码方案:

波形编码的话音质量高,但编码速率也很高(WAV);

参数编码的编码速率很低,产生的合成语音的音质不高(MP3);

混合编码使用参数编码技术和波形编码技术,编码速率和音质介于它们之间。

方案名词介绍:

波形编码PCM

波形编码是基于对语音信号波形的数字化处理,试图使处理后重建的语音信号波形与原语音信号波形保持一致。波形编码的优点是实现简单、语音质量较好、适应性强等;缺点是话音信号的压缩程度不是很高,实现的码速率比较高。常见的波形压缩编码方法有脉冲编码调制(PCM)

参数编码MP3

MP3文件其实是一种经过MP3(即动态影像专家压缩标准音频层面)编码算法压缩的数据,不能直接送给功放,必须先通过解码还原出原始音频数据再进行播放。

PWM原理

脉宽调制(PWM)基本原理:控制方式就是对逆变电路开关器件的通断进行控制,使输出端得到一系列幅值相等的脉冲,用这些脉冲来代替正弦波或所需要的波形。也就是在输出波形的半个周期中产生多个脉冲,使各脉冲的等值电压为正弦波形,所获得的输出平滑且低次谐波少。按一定的规则对各脉冲的宽度进行调制,即可改变逆变电路输出电压的大小,也可改变输出频率。例如,把正弦半波波形分成N等份,就可把正弦半波看成由N个彼此相连的脉冲所组成的波形。这些脉冲宽度相等,都等于 π/n ,但幅值不等,且脉冲顶部不是水平直线,而是曲线,各脉冲的幅值按正弦规律变化。如果把上述脉冲序列用同样数量的等幅而不等宽的矩形脉冲序列代替,使矩形脉冲的中点和相应正弦等分的中点重合,且使矩形脉冲和相应正弦部分面积(即冲量)相等,就得到一组脉冲序列,这就是PWM波形。可以看出,各脉冲宽度是按正弦规律变化的。根据冲量相等效果相同的原理,PWM波形和正弦半波是等效的。对于正弦的负半周,也可以用同样的方法得到PWM波形。

在PWM波形中,各脉冲的幅值是相等的,要改变等效输出正弦波的幅值时,只要按同一比例系数改变各脉冲的宽度即可,因此在交-直-交变频器中,PWM逆变电路输出的脉冲电压就是直流侧电压的幅值。

代码示例

MCU裸板开发

  1 #include <reg52.h>  
  2 #include <intrins.h>  
  3 #define uchar unsigned char  
  4 #define uint  unsigned int  
  5 //录音和放音键IO口定义:  
  6 sbit   AN=P2^6;//放音键控制接口  
  7 sbit    set_key=P2^7;//录音键控制口  
  8 // ISD4004控制口定义:  
  9 sbit SS  =P1^0;     //4004片选  
 10 sbit MOSI=P1^1;     //4004数据输入  
 11 sbit MISO=P1^2;     //4004数据输出  
 12 sbit SCLK=P1^3;     //ISD4004时钟  
 13 sbit INT =P1^4;     //4004中断  
 14 sbit STOP=P3^4;     //4004复位  
 15 sbit LED1 =P1^6;    //录音指示灯  
 16 //===============================LCD1602接口定义=====================  
 17 /*-----------------------------------------------------  
 18        |DB0-----P2.0 | DB4-----P2.4 | RW-------P0.1    |  
 19        |DB1-----P2.1 | DB5-----P2.5 | RS-------P0.2    |  
 20        |DB2-----P2.2 | DB6-----P2.6 | E--------P0.0    |  
 21        |DB3-----P2.3 | DB7-----P2.7 | 注意,P0.0到P0.2需要接上拉电阻  
 22     ---------------------------------------------------  
 23 =============================================================*/ 
 24 #define LCM_Data     P0    //LCD1602数据接口  
 25 sbit    LCM_RW     = P2^3;  //读写控制输入端,LCD1602的第五脚  
 26 sbit    LCM_RS     = P2^4;  //寄存器选择输入端,LCD1602的第四脚  
 27 sbit    LCM_E      = P2^2;  //使能信号输入端,LCD1602的第6脚  
 28 //***************函数声明************************************************  
 29 void    WriteDataLCM(uchar WDLCM);//LCD模块写数据  
 30 void    WriteCommandLCM(uchar WCLCM,BuysC); //LCD模块写指令  
 31 uchar   ReadStatusLCM(void);//读LCD模块的忙标  
 32 void    DisplayOneChar(uchar X,uchar Y,uchar ASCII);//在第X+1行的第Y+1位置显示一个字符  
 33 void    LCMInit(void);  
 34 void    DelayUs(uint us); //微妙延时程序  
 35 void    DelayMs(uint Ms);//毫秒延时程序  
 36 void    init_t0();//定时器0初始化函数  
 37 void    setkey_treat(void);//录音键处理程序  
 38 void    upkey_treat(void);//播放键处理程序  
 39 void    display();//显示处理程序  
 40 void    isd_setrec(uchar adl,uchar adh);//发送setrec指令  
 41 void    isd_rec();//发送rec指令  
 42 void    isd_stop();//stop指令(停止当前操作)  
 43 void    isd_powerup();//发送上电指令  
 44 void    isd_stopwrdn();//发送掉电指令  
 45 void    isd_send(uchar isdx);//spi串行发送子程序,8位数据  
 46 void    isd_setplay(uchar adl,uchar adh);  
 47 void    isd_play();  
 48 //程序中的一些常量定义  
 49 uint    time_total,st_add,end_add=0;  
 50 uint    adds[25];//25段语音的起始地址暂存  
 51 uint    adde[25];//25段语音的结束地址暂时  
 52 uchar   t0_crycle,count,count_flag,flag2,flag3,flag4;  
 53 uchar   second_count=170,msecond_count=0;  
 54 //second_count为芯片录音的起始地址,起始地址本来是A0,也就是160,  
 55 //我们从170开始录音吧。  
 56 #define Busy         0x80   //用于检测LCM状态字中的Busy标识  
 57  
 58 /*===========================================================================  
 59  主程序  
 60 =============================================================================*/  
 61 void main(void)  
 62 {  
 63    LED1=0;//灭录音指示灯  
 64    flag3=0;  
 65    flag4=0;  
 66    time_total=340;//录音地址从170开始,对应的单片机开始计时的时间就是340*0.1秒  
 67    adds[0]=170;  
 68    count=0;  
 69    LCMInit();        //1602初始化  
 70    init_t0();//定时器初始化  
 71    DisplayOneChar( 0,5,'I'); //开机时显示000  ISD4004-X  
 72    DisplayOneChar( 0,6,'S');  
 73    DisplayOneChar( 0,7,'D');  
 74    DisplayOneChar( 0,8,'4');  
 75    DisplayOneChar( 0,9,'0');  
 76    DisplayOneChar( 0,10,'0');  
 77    DisplayOneChar( 0,11,'4');  
 78    DisplayOneChar( 0,12,'-');  
 79    DisplayOneChar( 0,13,'X');  
 80    while(1)  
 81    {  
 82       display();//显示处理  
 83       upkey_treat();//放音键处理  
 84       setkey_treat();//录音键处理  
 85    }  
 86 }  
 87 //*******************************************  
 88 //录音键处理程序  
 89 //从指定地址开始录音的程序就是在这段里面  
 90 void setkey_treat(void)  
 91 {  
 92    set_key=1;//置IO口为1,准备读入数据  
 93    DelayUs(1);  
 94    if(set_key==0)  
 95    {  
 96       if(flag3==0)//录音键和放音键互锁,录音好后,禁止再次录音。如果要再次录音,那就要复位单片机,重新开始录音  
 97       {  
 98         if(count==0)//判断是否为上电或复位以来第一次按录音键  
 99         {  
100            st_add=170;  
101         }  
102         else 
103         {  
104           st_add=end_add+3;   
105         }//每段语言间隔3个地址  
106         adds[count]=st_add;//每段语音的起始地址暂时  
107         if(count>=25)//判断语音段数时候超过25段,因为单片机内存的关系?  
108        //本程序只录音25段,如果要录更多的语音,改为不可查询的即可  
109         {//如果超过25段,则覆盖之前的语音,从新开始录音  
110            count=0;  
111            st_add=170;  
112            time_total=340;  
113         }  
114         isd_powerup(); //AN键按下,ISD上电并延迟50ms  
115         isd_stopwrdn();  
116         isd_powerup();   
117         LED1=1;//录音指示灯亮,表示录音模式  
118         isd_setrec(st_add&0x00ff,st_add>>8); //从指定的地址  
119         if(INT==1)// 判定芯片有没有溢出  
120         {         
121             isd_rec(); //发送录音指令  
122         }  
123         time_total=st_add*2;//计时初始值计算  
124         TR0=1;//开计时器  
125         while(set_key==0);//等待本次录音结束  
126         TR0=0;//录音结束后停止计时  
127         isd_stop(); //发送4004停止命令  
128         end_add=time_total/2+2;//计算语音的结束地址  
129         adde[count]=end_add;//本段语音结束地址暂存  
130         LED1=0; //录音完毕,LED熄灭  
131         count++;//录音段数自加  
132         count_flag=count;//录音段数寄存  
133         flag2=1;  
134         flag4=1;//解锁放音键  
135       }  
136   }  
137 }  
138 //=================================================  
139 //放音机处理程序  
140 //从指定地址开始放本段语音就是这段程序  
141 void upkey_treat(void)  
142 {  
143    uchar ovflog;  
144    AN=1;//准备读入数据  
145    DelayUs(1);  
146    if(AN==0)//判断放音键是否动作  
147    {  
148  //    if(flag4==1)//互锁录音键  
149  //    {  
150         if(flag2==1)//判断是否为录音好后的第一次放音  
151         {  
152            count=0;//从第0段开始播放  
153         }  
154         isd_powerup(); //AN键按下,ISD上电并延迟50ms  
155         isd_stopwrdn();  
156         isd_powerup();   
157         //170 184 196 211  
158    //     st_add=adds[count];//送当前语音的起始地址  
159         st_add=211;//送当前语音的起始地址  
160         isd_setplay(st_add&0x00ff,st_add>>8); //发送setplay指令,从指定地址开始放音  
161         isd_play(); //发送放音指令  
162         DelayUs(20);  
163         while(INT==1); //等待放音完毕的EOM中断信号  
164         isd_stop(); //放音完毕,发送stop指令  
165         while(AN==0); //   
166         isd_stop();  
167         count++;//语音段数自加  
168         flag2=0;  
169         flag3=1;  
170         if(count>=count_flag)//如果播放到最后一段后还按加键,则从第一段重新播放  
171         {  
172              count=0;  
173         }  
174        
175  //     }  
176    }   
177 }  
178 //************************************************?  
179 //发送rec指令  
180 void isd_rec()  
181 {  
182     isd_send(0xb0);  
183     SS=1;  
184 }  
185 //****************************************  
186 //发送setrec指令  
187 void isd_setrec(unsigned char adl,unsigned char adh)  
188 {  
189     DelayMs(1);  
190     isd_send(adl); //发送放音起始地址低位  
191     DelayUs(2);  
192     isd_send(adh); //发送放音起始地址高位  
193     DelayUs(2);  
194     isd_send(0xa0); //发送setplay指令字节  
195     SS=1;  
196 }  
197 //=============================================================================  
198 //**********************************************  
199 //定时器0中断程序  
200 void timer0() interrupt 1  
201 {  
202     TH0=(65536-50000)/256;  
203     TL0=(65536-50000)%256;  
204     t0_crycle++;  
205     if(t0_crycle==2)// 0.1秒  
206     {  
207       t0_crycle=0;  
208       time_total++;  
209       msecond_count++;  
210       if(msecond_count==10)//1秒  
211       {   
212         msecond_count=0;  
213         second_count++;  
214         if(second_count==60)  
215         {  
216           second_count=0;  
217         }  
218       }  
219       if(time_total==4800)time_total=0;      
220     }  
221 }  
222 //********************************************************************************************  
223 //定时器0初始化函数  
224 void init_t0()  
225 {  
226     TMOD=0x01;//设定定时器工作方式1,定时器定时50毫秒  
227     TH0=(65536-50000)/256;  
228     TL0=(65536-50000)%256;  
229     EA=1;//开总中断  
230     ET0=1;//允许定时器0中断  
231     t0_crycle=0;//定时器中断次数计数单元  
232 }  
233 //******************************************  
234 //显示处理程序  
235 void display()  
236 {  
237         uchar x;  
238         if(flag3==1||flag4==1)//判断是否有录音过或者放音过  
239         {  
240           x=count-1;  
241           if(x==255){x=count_flag-1;}  
242         }  
243         DisplayOneChar( 0,0,x/100+0x30);    //显示当前语音是第几段  
244         DisplayOneChar( 0,1,x/10%10+0x30);  
245         DisplayOneChar( 0,2,x%10+0x30);  
246         if(flag3==0)//录音时显示本段语音的起始和结束地址  
247         {  
248            DisplayOneChar( 1,0,st_add/1000+0x30);//计算并显示千位     
249            DisplayOneChar( 1,1,st_add/100%10+0x30);  
250            DisplayOneChar( 1,2,st_add/10%10+0x30);  
251            DisplayOneChar( 1,3,st_add%10+0x30);  
252            DisplayOneChar( 1,4,'-');  
253            DisplayOneChar( 1,5,'-');  
254            DisplayOneChar( 1,6,end_add/1000+0x30);     
255            DisplayOneChar( 1,7,end_add/100%10+0x30);  
256            DisplayOneChar( 1,8,end_add/10%10+0x30);  
257            DisplayOneChar( 1,9,end_add%10+0x30);  
258         }  
259         if(flag4==1)//放音时显示本段语音的起始和结束地址  
260         {  
261            DisplayOneChar( 1,0,adds[x]/1000+0x30);     
262            DisplayOneChar( 1,1,adds[x]/100%10+0x30);  
263            DisplayOneChar( 1,2,adds[x]/10%10+0x30);  
264            DisplayOneChar( 1,3,adds[x]%10+0x30);  
265            DisplayOneChar( 1,4,'-');  
266            DisplayOneChar( 1,5,'-');  
267            DisplayOneChar( 1,6,adde[x]/1000+0x30);     
268            DisplayOneChar( 1,7,adde[x]/100%10+0x30);  
269            DisplayOneChar( 1,8,adde[x]/10%10+0x30);  
270            DisplayOneChar( 1,9,adde[x]%10+0x30);  
271         }  
272 }  
273 //======================================================================  
274 // LCM初始化  
275 //======================================================================  
276 void LCMInit(void)   
277 {  
278  LCM_Data = 0;  
279  WriteCommandLCM(0x38,0); //三次显示模式设置,不检测忙信号  
280  DelayMs(5);  
281  WriteCommandLCM(0x38,0);  
282  DelayMs(5);  
283  WriteCommandLCM(0x38,0);  
284  DelayMs(5);  
285  WriteCommandLCM(0x38,1); //显示模式设置,开始要求每次检测忙信号  
286  WriteCommandLCM(0x08,1); //关闭显示  
287  WriteCommandLCM(0x01,1); //显示清屏  
288  WriteCommandLCM(0x06,1); // 显示光标移动设置  
289  WriteCommandLCM(0x0C,1); // 显示开及光标设置  
290  DelayMs(100);  
291 }  
292 //*=====================================================================  
293 // 写数据函数: E =高脉冲 RS=1 RW=0  
294 //======================================================================  
295 void WriteDataLCM(uchar WDLCM)  
296 {  
297  ReadStatusLCM(); //检测忙  
298  LCM_Data = WDLCM;  
299  LCM_RS = 1;  
300  LCM_RW = 0;  
301  LCM_E = 0; //若晶振速度太高可以在这后加小的延时  
302  LCM_E = 0; //延时  
303  LCM_E = 1;  
304 }  
305 //*====================================================================  
306  // 写指令函数: E=高脉冲 RS=0 RW=0  
307 //======================================================================  
308 void WriteCommandLCM(unsigned char WCLCM,BuysC) //BuysC为0时忽略忙检测  
309 {  
310  if (BuysC) ReadStatusLCM(); //根据需要检测忙  
311  LCM_Data = WCLCM;  
312  LCM_RS = 0;  
313  LCM_RW = 0;  
314  LCM_E = 0;  
315  LCM_E = 0;  
316  LCM_E = 1;  
317 }  
318 //*====================================================================  
319 //  正常读写操作之前必须检测LCD控制器状态:E=1 RS=0 RW=1;  
320 //  DB7: 0 LCD控制器空闲,1 LCD控制器忙。  
321  // 读状态  
322 //======================================================================  
323 unsigned char ReadStatusLCM(void)  
324 {  
325  LCM_Data = 0xFF;  
326  LCM_RS = 0;  
327  LCM_RW = 1;  
328  LCM_E = 0;  
329  LCM_E = 0;  
330  LCM_E = 1;  
331  while (LCM_Data & Busy); //检测忙信号    
332  return(LCM_Data);  
333 }  
334 //======================================================================  
335 //功 能:     在1602 指定位置显示一个字符:第一行位置0~15,第二行16~31  
336 //说 明:     第 X 行,第 y 列  注意:字符串不能长于16个字符  
337 //======================================================================  
338 void DisplayOneChar( unsigned char X, unsigned char Y, unsigned char ASCII)  
339 {  
340  X &= 0x1;  
341  Y &= 0xF; //限制Y不能大于15,X不能大于1  
342  if (X) Y |= 0x40; //当要显示第二行时地址码+0x40;  
343  Y |= 0x80; // 算出指令码  
344  WriteCommandLCM(Y, 0); //这里不检测忙信号,发送地址码  
345  WriteDataLCM(ASCII);  
346 }  
347 //======================================================================  
348 //spi串行发送子程序,8位数据  
349 void isd_send(uchar isdx)  
350 {  
351     uchar isx_counter;  
352     SS=0;//ss=0,打开spi通信端  
353     SCLK=0;  
354     for(isx_counter=0;isx_counter<8;isx_counter++)//先发低位再发高位,依次发送。  
355     {  
356         if((isdx&0x01)==1)  
357             MOSI=1;  
358         else 
359             MOSI=0;  
360             isdx=isdx>>1;  
361             SCLK=1;  
362             DelayUs(2);  
363             SCLK=0;  
364             DelayUs(2);  
365     }  
366 }  
367 //======================================================================  
368 //stop指令(停止当前操作)  
369 void isd_stop()//  
370 {  
371     DelayUs(10);  
372     isd_send(0x30);  
373     SS=1;  
374     DelayMs(50);  
375 }  
376 //======================================================================  
377 //发送上电指令  
378 void isd_powerup()//  
379 {  
380     DelayUs(10);  
381     SS=0;  
382     isd_send(0x20);  
383     SS=1;  
384     DelayMs(50);  
385 }  
386 //======================================================================  
387 //发送掉电指令  
388 void isd_stopwrdn()//  
389 {  
390     DelayUs(10);  
391     isd_send(0x10);  
392     SS=1;  
393     DelayMs(50);  
394 }  
395  
396 void isd_play()//发送play指令  
397 {  
398     isd_send(0xf0);  
399     SS=1;  
400 }  
401 void isd_setplay(uchar adl,uchar adh)//发送setplay指令  
402 {  
403     DelayMs(1);  
404     isd_send(adl); //发送放音起始地址低位  
405     DelayUs(2);  
406     isd_send(adh); //发送放音起始地址高位  
407     DelayUs(2);  
408     isd_send(0xe0); //发送setplay指令字节  
409     SS=1;  
410 }  
411 void DelayUs(uint us)  
412 {  
413     while(us--);  
414 }   
415 //====================================================================  
416 // 设定延时时间:x*1ms  
417 //====================================================================  
418 void DelayMs(uint Ms)  
419 {  
420   uint i,TempCyc;  
421   for(i=0;i<Ms;i++)  
422   {  
423     TempCyc = 250;  
424     while(TempCyc--);  
425   }  
426 }  
427  
原文  http://www.cnblogs.com/lzc978/p/12745317.html
正文到此结束
Loading...