解析的Wave 驱动的架构。我们了解一个驱动的时候,先不去看具体跟硬件操作相关的东西,而是从流程入手,把整个流程搞清楚了,调试起来就非常的容易了。我们着重看hwctxt.cpp,hwctxt.H,devctxt.cpp,devctxt.H,strmctxt.cpp,strmctxt.H这几个源文件。其中hwctxt是类HardwareContext代码文件,devctxt是DeviceContext代码文件,strmctxt是StreamContext代码文件。这几个类的其他一些功能,还在其他一些文件中实现,如output.Cpp,midistrm.Cpp等。

现在我们来看下StreamContext的类图,StreamContext是管理音频流的对象,包括播放、暂停、停止、设置音量、获取播放位置等。从下面的StreamContext的类图中,我们可以看到它派生了WaveStreamContext和MidiStream。然后WaveStreamContext又派生了Input和Output类型的Stream。不用说也可以知道InputStreamContext是针对于像麦克这种输入设备流的。

StreamContext类图

其中OutputStreamContext派生了六个类,M代表单音道,S代表的是立体音,8/16是8/16比特采样了。 SPDIF(SONY/PHILIPS DIGITAL INTERFACE)是一种最新的音频传输格式,它通过光纤进行数字音频信号传输以取代传统的模拟信号传输方式,因此可以取得更高质量的音质效果。

StreamContext是一个管理音频数据流的对象,像智能手机中可能存在用media player播放音乐,同时又开着FM,突然又来电。从上篇文章中我们知道,要想调用wave驱动的播放功能,每个应用都有一份StreamContext对象,上面提到的状况,就会有三个StreamContext对象被创建。 在硬件只要一个的条件下,那么这三个StreamContext是如果协同工作的呢?而DeviceContext正是管理StreamContext对象的。

如下是DeviceContext类图:


DeviceContext类图

DeviceContext派生出InputDeviceContext和OutputDeviceContext,他们分别管理InputStreamContext和OutputStreamContext。在DeviceContext内部维护了一个双向链表来管理StreamContext。

HardwareContext是具体操作硬件相关的类,其内部包含InputDeviceContext和OutputDeviceContext对象,下面这种图,就是三个类的关系图,一看就知道他们的对应关系了。

DeivceContext和StreamContext关系图

对于HardwareContext是具体操作硬件的东西,不具有代码性,只要仔细看看代码就行了。现在我们主要分析下DeviceContext和StreamContext的关系。

DeviceContext的作用是管理StreamContext,可以分为几套函数,见Devctxt.h, Devctxt.cpp

音量增益管理:下面这个函数主要是设置设备的整个音量增益,设置了设备音量增益后,对流音量的增益起了限制做用的。

音量函数如下

[cpp] view plaincopyprint?
  1. DWORD GetGain();
  2. DWORD SetGain(DWORD dwGain);
  3. DWORD GetDefaultStreamGain();
  4. DWORD SetDefaultStreamGain(DWORD dwGain);
  5. DWORD GetSecondaryGainLimit(DWORD GainClass);
  6. DWORD SetSecondaryGainLimit(DWORD GainClass, DWORD Limit);

先来讲下设备音量增益(Device Gain)和流音量增益(Stream Gain)的关系。我们从微软Media Player中,很容易就看到了设备音量和流音量的关系。设备音量时通过音量键来控制系统的音量,从而改变整个输出设备的音量的,但是在Media Player中,还是有一个单独的音量控制按钮,它能调节Media Player的音量(不要问我在哪里,自己找),但是调试它是受限制于系统音量,是如何限制,请看下面讲解。

我们现在看下设置系统音量和设置流音量的整个流程,来了解整个音量控制的过程。用户设置时,会调用waveOutSetVolume

MMRESULT waveOutSetVolume(

HWAVEOUT hwo,

DWORD dwVolume

);

当HWAVEOUT传入为空时,设置的就是设备音量,当HWAVEOUT是通过调用waveOutOpen返回的句柄是,设置的就是流音量。

好,我们进入到驱动中区看看,waveOutSetVolume会调用到来看wavemain.Cpp中HandleWaveMessage的WODM_SETVOLUME分支,我在代码中去掉了不重要的部分,可以看得更清晰些。

[cpp] view plaincopyprint?
  1. case WODM_SETVOLUME:
  2. {
  3. StreamContext *pStreamContext;
  4. pStreamContext = (StreamContext *) dwUser;
  5. LONG dwGain = dwParam1;
  6. if (pStreamContext)
  7. {
  8. dwRet = pStreamContext->SetGain(dwGain);
  9. }
  10. else
  11. {
  12. DeviceContext *pDeviceContext = g_pHWContext->GetOutputDeviceContext(uDeviceId);
  13. dwRet = pDeviceContext->SetGain(dwGain);
  14. }
  15. }

dwUser 指向的是StreamContext对象(在前文中已经讲过),如果pStreamContext为空,那么就调用DeviceContext的SetGain函数,否则调用StreamContext的SetGain函数。调用StreamContext的Gain只对当前的StreamContext的音量起作用,不影响其他的Stream音量。但是对DeviceContext设置音量增益是对DeviceContext管理的所有StreamContext起了控制作用,但是具体是如何影响的,还是根据代码来分析:

在Devctxt.h中的SetGain函数代码如下

[cpp] view plaincopyprint?
  1. DWORD SetGain(DWORD dwGain)
  2. {
  3. m_dwGain = dwGain;
  4. RecalcAllGains();
  5. return MMSYSERR_NOERROR;
  6. }

用m_dwGain保存设备音量,然后调用RecalcAllGains来重新计算所有StreamContext的音量增益。

在Devctxt.cpp中的RecalcAllGains的实现如下

[cpp] view plaincopyprint?
  1. void DeviceContext::RecalcAllGains()
  2. {
  3. PLIST_ENTRY pListEntry;
  4. StreamContext *pStreamContext;
  5. for (pListEntry = m_StreamList.Flink;
  6. pListEntry != &m_StreamList;
  7. pListEntry = pListEntry->Flink)
  8. {
  9. pStreamContext = CONTAINING_RECORD(pListEntry,StreamContext,m_Link);
  10. pStreamContext->GainChange();
  11. }
  12. return;
  13. }

它便利所有的StreamContext,并调用pStreamContext->GainChange()来改变StreamContext对象的音量。接着看StreamContext类中的GainChange的实现

[cpp] view plaincopyprint?
  1. void GainChange()
  2. {
  3. m_fxpGain = MapGain(m_dwGain);
  4. }
  5. DWORD StreamContext::MapGain(DWORD Gain)
  6. {
  7. DWORD TotalGain = Gain & 0xFFFF;
  8. DWORD SecondaryGain = m_pDeviceContext->GetSecondaryGainLimit(m_SecondaryGainClass) & 0xFFFF;
  9. if (m_SecondaryGainClass < SECONDARYDEVICEGAINCLASSMAX)
  10. {
  11. // Apply device gain
  12. DWORD DeviceGain = m_pDeviceContext->GetGain() & 0xFFFF;
  13. TotalGain *= DeviceGain;
  14. TotalGain += 0xFFFF;  // Round up
  15. TotalGain >>= 16;     // Shift to lowest 16 bits
  16. }
  17. // Apply secondary gain
  18. TotalGain *= SecondaryGain;
  19. TotalGain += 0xFFFF;  // Round up
  20. TotalGain >>= 16;     // Shift to lowest 16 bits
  21. // Special case 0 as totally muted
  22. if (TotalGain==0)
  23. {
  24. return 0;
  25. }
  26. // Convert to index into table
  27. DWORD Index = 63 - (TotalGain>>10);
  28. return GainMap[Index];
  29. }

音量在系统中用一个DWORD值来表示,其高低两个字节分别来表示左右声道,一般情况下左声道和右声道的音量大小是一样的,所以只取其低两个字节,DWORD TotalGain = Gain & 0xFFFF;

TotalGain是DeviceGain和m_dwGain的乘机,然后再左移16位得到的。其实就是TotalGain=DeviceGain*m_dwGain/最高音量,如果把DeviceGain/最高音量,用百分比来算的话,就很更容易理解了,那么最后的公式就变成TotalGain=DeviceGain*系统音量百分比。那么这里就解释了系统音量是如何限制流音量的疑问。

我们设置好音量增益后,最终会再哪里体现呢:首先看一下Output.cpp文件,WaveStreamContext::Render之后的数据就是直接发送到外部声音芯片的数据,他根据参数以及标志位选择OutputStreamContextXXX::Render2,XXX表示双声道S单声道M,bit位是8位还是16位。以双声道OutputStreamContextS16::Render2为例,BSP里面的代码如下:

[cpp] view plaincopyprint?
  1. PBYTE OutputStreamContextS16::Render2(PBYTE pBuffer, PBYTE pBufferEnd, PBYTE pBufferLast)
  2. {
  3. LONG CurrT = m_CurrT;
  4. LONG DeltaT = m_DeltaT;
  5. LONG CurrSamp0 = m_CurrSamp[0];
  6. LONG PrevSamp0 = m_PrevSamp[0];
  7. PBYTE pCurrData = m_lpCurrData;
  8. PBYTE pCurrDataEnd = m_lpCurrDataEnd;
  9. LONG fxpGain = m_fxpGain;
  10. LONG OutSamp0;
  11. __try
  12. {
  13. while (pBuffer < pBufferEnd)
  14. {
  15. while (CurrT >= 0x100)
  16. {
  17. if (pCurrData>=pCurrDataEnd)
  18. {
  19. goto Exit;
  20. }
  21. CurrT -= 0x100;
  22. PrevSamp0 = CurrSamp0;
  23. PPCM_SAMPLE pSampleSrc = (PPCM_SAMPLE)pCurrData;
  24. CurrSamp0 =  (LONG)pSampleSrc->s16.sample_left;
  25. CurrSamp0 += (LONG)pSampleSrc->s16.sample_right;
  26. CurrSamp0 = CurrSamp0>>1;
  27. pCurrData+=4;
  28. }
  29. OutSamp0 = PrevSamp0 + (((CurrSamp0 - PrevSamp0) * CurrT) >> 8);
  30. // 设置增益
  31. OutSamp0 = (OutSamp0 * fxpGain) >> VOLSHIFT;
  32. CurrT += DeltaT;
  33. if (pBuffer < pBufferLast)
  34. {
  35. OutSamp0 += *(HWSAMPLE *)pBuffer;
  36. }
  37. *(HWSAMPLE *)pBuffer = (HWSAMPLE)OutSamp0;
  38. pBuffer += sizeof(HWSAMPLE);
  39. }
  40. }//end the __try block
  41. __except (EXCEPTION_EXECUTE_HANDLER)
  42. {
  43. RETAILMSG(1, (TEXT("InputStreamContext::Render2!/r/n")));
  44. m_lpCurrData = m_lpCurrDataEnd = NULL;
  45. return NULL;
  46. }
  47. Exit:
  48. m_dwByteCount += (pCurrData - m_lpCurrData);
  49. m_lpCurrData = pCurrData;
  50. m_CurrT = CurrT;
  51. m_PrevSamp[0] = PrevSamp0;
  52. m_CurrSamp[0] = CurrSamp0;
  53. return pBuffer;
  54. }

从上面看到是与采样数据相乘,然后在左移16位。跟上面提到的系统音量影响流音量是一样的。

上面讲了,DeviceContext的音量增益管理,现在来看下它的流管理。

StreamContext流管理:主要来管理StreamContext的创建、删除、渲染、传输等功能。

主要有如下几个函数

[cpp] view plaincopyprint?
  1. StreamContext *CreateStream(LPWAVEOPENDESC lpWOD);
  2. DWORD OpenStream(LPWAVEOPENDESC lpWOD, DWORD dwFlags, StreamContext **ppStreamContext);
  3. HRESULT Open(DeviceContext *pDeviceContext, LPWAVEOPENDESC lpWOD, DWORD dwFlags);
  4. void NewStream(StreamContext *pStreamContext);
  5. void DeleteStream(StreamContext *pStreamContext);
  6. void StreamReadyToRender(StreamContext *pStreamContext);
  7. PBYTE TransferBuffer(PBYTE pBuffer, PBYTE pBufferEnd, DWORD *pNumStreams, BOOL bMuteFlag);

在DeviceContext中有个m_StreamList的双向链表(LIST_ENTRY), m_StreamList用来指向链表的头。在StreamConext中也存在一个m_Link(LIST_ENTRY)。StreamContext是调用DeviceContext的OpenStream来创建的,然后把StreamContext对象加入到DeviceContext的m_StreamList中。我们从代码中去直接分析:

上层调用waveoutOpen,在wavedev2中会调用WODM_OPEN这个分支。在WODM_OPEN中的代码如下:

[cpp] view plaincopyprint?
  1. case WODM_OPEN:
  2. {
  3. StreamContext *pStreamContext;
  4. pStreamContext = (StreamContext *) dwUser;
  5. dwRet = pDeviceContext->OpenStream((LPWAVEOPENDESC)dwParam1, dwParam2, (StreamContext **)pStreamContext);
  6. break;
  7. }

OpenStream的其流程图如下

StreamContext 初始化流程

CreateStream是根据WAVEFORMATEX这个结构体,来判断具体要创建StreamContext的哪个派生类,下面是CreateStream的流程图,不可不提,还是流程图清晰。

OutputDeviceContext:: CreateStream流程图

上面讲了上层通过WODM_OPEN创建一个StreamContext的过程,那么音频流被打开之后,接下来就是给StreamContext传入音频数据开始播放音乐。Wavedev2提供了WODM_WRITE来向音频设置写入数据。我们先看下WODM_WRITE分支的代码

[cpp] view plaincopyprint?
  1. case WODM_WRITE:
  2. {
  3. StreamContext *pStreamContext;
  4. pStreamContext = (StreamContext *) dwUser;
  5. dwRet = pStreamContext->QueueBuffer((LPWAVEHDR)dwParam1);
  6. break;
  7. }

这里调用了StreamContext中的QueueBuffer,QueueBuffer的作用就是把WAVEHDR中的数据加入到StreamContext的队列中,等待播放。下面是QueueBuffer的流程图

QueueBuffer流程图

在QueueBuffer中调用DeviceContext中的StreamReadyToReander通知可以开始渲染了,流程图中的箭头方向是StreamReadyToReander调用流程,最终调用SetEvent(hOutputIntEvent),来通知线程数据已经准备好,得到通知后,就开始播放了。该线程在HardwareContext中的OutputInterruptThread函数中

OutputInterruptThread流程如下

WinCE平台上的DMA

CEDDK提供了DMA的相关函数,在CEDDK/DDK_DMA/ddk_dma.c中定义。最有用的就两个函数,HalAllocateCommonBuffer(..)和HalFreeCommonBuffer(..)分别用于为DMA申请和释放内存。

(1)首先介绍一下会用到的DMA适配器结构,在ceddk.h中定义,如下:

typedef struct _DMA_ADAPTER_OBJECT_ 

      USHORT ObjectSize;               //该结构的大小 
      INTERFACE_TYPE InterfaceType;    //接口类型,一般用做DMA时设置为Internal 
      ULONG BusNumber;                 //一般设置为0 
} DMA_ADAPTER_OBJECT, *PDMA_ADAPTER_OBJECT;

(2)DMA内存分配函数:

PVOID HalAllocateCommonBuffer(PDMA_ADAPTER_OBJECT DmaAdapter, ULONG Length, PPHYSICAL_ADDRESS LogicalAddress, BOOLEAN CacheEnabled)

DmaAdapter:        DMA适配器结构指针

Length:                 要分配的内存的大小

LogicalAddress:    分配成功后,内存的物理起始地址

CacheEnabled:     是否使用Cache

实际上该函数通过调用AllocPhysMem函数来分配一段物理地址连续的内存,这段内存默认是64KB字节对齐的,DMA操作的物理内存必须是连续的。该函数调用成功以后,返回值是虚拟地址,可以在驱动中访问其中的内容,函数的第三个参数返回内存的物理地址,可以赋值给DMA控制器来完成DMA操作。

(3)DMA内存释放函数:

VOID HalFreeCommonBuffer(PDMA_ADAPTER_OBJECT DmaAdapter, ULONG Length, PHYSICAL_ADDRESS LogicalAddress, PVOID VirtualAddress, BOOLEAN CacheEnabled)

DmaAdapter:        DMA适配器结构指针

Length:                 内存的大小

LogicalAddress:    内存的物理起始地址

VirtualAddress:     内存的虚拟地址

CacheEnabled:      是否使用Cache

该函数通过调用FreePhysMem函数来完成内存的释放,所以在使用该函数的时候,只有函数的第四个参数是必须的,也就是内存的虚拟地址,其他的都可以忽略。

(4)下面给个使用上面两个函数的例子:

DMA_ADAPTER_OBJECT dmaAdapter; 
//初始化DMA适配器 
dmaAdapter.ObjectSize = sizeof(dmaAdapter); 
dmaAdapter.InterfaceType = Internal; 
dmaAdapter.BusNumber = 0; 
//分配DMA内存 
m_pDMABuf = (PBYTE)HalAllocateCommonBuffer( &dmaAdapter, 256 * 1024, &m_pDMABufPhys, FALSE );
//将物理地址赋值给DMA控制器 
vm_pDMAreg->DST = (int)m_pDMABufPhys.LowPart;

...

//释放DMA内存 
if( m_pDMABuf != NULL ) 

     HalFreeCommonBuffer( NULL, 0, 0, m_pDMABuffer, FALSE); 
     m_pDMABuf = NULL; 
}

在ddk_dma.c中,还可以看到其他很多DMA相关的操作函数。这些DMA函数是用来操作DMA设备的,通过CreateFile来打开DMA设备,然后调用DeviceIoControl函数来访问DMA设备。DMA设备驱动在/WINCE600/PUBLIC/Common/Oak/Drivers/DMA下面,该DMA驱动以流设备驱动的形式实现。

一般来说,DMA驱动会配合其他设备驱动来完成数据传输,所以很少会被单独作为一个设备来使用,大多数情况我们开发设备驱动时需要用到DMA的时候,会用到上面两个函数来申请和释放内存。

(5)音频驱动中的DMA

以S3C2440A为例,它的DMA控制器没有内置的DMA存储区域,所以驱动程序必须在内存内为音频设备分配DMA缓冲区。缓冲区设置是否合理非常关键,缓冲区太小容易造成缓冲区溢出,而要填充大的缓冲区,CPU就要一次处理大量的数据,容易造成延迟。

所以在本驱动中采用双缓冲区来解决这个问题,也就是当CPU在处理某一个缓冲区音频数据的同时,DMA控制器可以完成另一个缓冲区音频数据的传输,如此交替下去,则可以提高系统的并行能力,提高音频处理的实时性。本驱动所采用的DMA1通道和DMA2通道分别设置了两个缓冲区。采用DMA控制器通道1控制录制的音频数据的传输,采用通道2控制播放的音频数据的传输。
      以放音为例,示意图如下:

新的音频数据在CPU的控制下先写到DMA缓冲区A中,此时DMA控制器正在从DMA缓冲区B中迁移音频数据到IIS总线。当缓冲区B的数据全部传输完成之后,DMA控制器产生INT_DMA2中断,该中断通知CPU开始往缓冲区B中写新的音频数据,与此同时DMA控制器从缓冲区A中迁移数据到IIS总线。这样交替循环,由于CPU和DMA控制器没有同时处理同一块缓冲区,就减少了资源访问的冲突,并且能够最大程度上提高音频处理的实时性。
      放音的协作过程:

A,DMA请求,开始播放音乐时,DMA控制器收到IIS的发送请求后,向CPU提出接管总线要求,以便进行下面的DMA数据传输。

B,DMA音频数据传输,DMA控制器得到总线的控制权后,通知IIS控制器DMA应答,这时开始进入DMA数据传输,DMA控制器从输出缓冲区A中取出CPU填充的音频数据到IIS控制器的发送FIFO,当前的DMA传输结束,也即2048个字节的音频数据已通过IIS总线发送到音频编解码器。

C,DMA中断,DMA传输结束后DMA控制器向CPU发出INT_DMA2中断,表示输出缓冲区A的数据已经被迁移到音频编解码器,这时CPU(这之前CPU往输出缓冲区B写入音频数据)转而向输出缓冲区A写入音频数据,而DMA控制器同时从输出缓冲区B中迁移数据到发送的FIFO。CPU和DMA控制器如此交替访问缓冲区,实现音频数据的快速传输。

录音时DMA的操作类似。

Devctxt.cpp
 器件关联——包含了音频流的创造,删除,打开,关闭,格式等功能
 
Hwctxt.cpp
 硬件关联——包含了基本的硬件功能在各个状态的全局配置
 
I2citf.cpp
 I2C传输配置
 
I2S.cpp
 I2S传输配置
 
Input.cpp
 负责输入音频流
 
Output.cpp
 负责输出音频流
 
Midinote.cpp
 负责输出MIDI
 
Midistrm.cpp
 负责MIDI的开关以及控制
 
Mixerdrv.cpp
 系统软件混音
 
RTcodecComm.cpp
 Wm8753的所有功能配置,以及初始化设置
 
Strmctxt.cpp
 负责所有音频流的增益,buffer请求等功能以及对Devctxt的控制
 
Wavemain.cpp
 包含了所有的流接口函数

wince 音频学习相关推荐

  1. Java中AudioFileStream_iOS音频学习一之AudioFileStream

    音乐一直是我的爱好,作为一名开发,同时我也想知道这些音乐是怎么播放的,音效是如何改变的,如何升降调,一个音乐播放器是怎么实现的.从而开启我的音频学习之路 基本知识 人耳所能听到的声音,最低的频率是从2 ...

  2. Qualcomm 音频学习一

    前言 最近在学习高通的音频驱动,在学习了高通音频 bring up 和 Audio overview 文档后,并在网上寻找到一篇比较重要的 blog进行学习后,将这部分学习笔记记录于此. 四个重要部分 ...

  3. Qualcomm 音频学习(Bring up)

    原址 Qualcomm Audio HAL 音频通路设置 前言 最近在学习高通的音频驱动,在学习了高通音频 bring up 和 Audio overview 文档后,并在网上寻找到一篇比较重要的 b ...

  4. 视音频学习基础篇(一)----YUV采样格式和存储格式

    先给自己打个广告,本人的微信公众号:嵌入式Linux江湖,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题. 本系列主要介绍视频中的 ...

  5. wince大排档学习

    norains相对我辈来说是个大家,专业人士.他的书籍对我帮助很大.现列出一些资源,这些大多是他本博客的内容.先表示感谢. <Windows CE大排档>源代码 http://blog.c ...

  6. Android音频学习之MediaExtractor,提取音频视频轨道数据(从视频中分离音频视频数据)

    一个音视频文件是由音频和视频组成的,我们可以通过MediaExtractor.MediaMuxer把音频或视频给单独抽取出来,抽取出来的音频和视频能单独播放: 1 MediaExtractor 说明 ...

  7. 【HoloLens2】添加空间音频学习笔记

    向HoloLens的Unity项目添加空间音频 本教程内容: 如何在Unity中的HoloLens 2上使用与头部相关的传递函数(HRTF)卸载 使用HRTF卸载时如何启用混响 在微软Spatiali ...

  8. wince 蓝牙 学习

    参考链接:http://blog.csdn.net/csu_yang/article/details/5912659 http://blog.csdn.net/lailzhihou/article/d ...

  9. 音频学习之-g711

    什么是g711 g711是一种由国际电信联盟制定的一套语音压缩标准,主要用于电话语音通信,而人声最大频率一般在3.4kHz,所以只要以8k的采样频率对人声进行采样,就可以保证完全还原原始声音. g71 ...

  10. 都来说说你是如何学习wince 驱动的(请大牛们也来凑凑热闹)

    我想大家也知道,论坛和一些QQ技术交流群很多新手都会问:应该如何学习wince驱动?以前很多时间,也打字打的手痛.也不敢说的太多,怕误人子弟.现在在这里开个帖子,希望老牛们不吝赐教新手,呵呵.大家照着 ...

最新文章

  1. javascript与DOM的渊源
  2. Zookeeper--Watcher机制源码剖析二
  3. [JZOJ 5911] [NOIP2018模拟10.18] Travel 解题报告 (期望+树形DP)
  4. AttributeError : module ‘enum‘ has no attribute ‘IntFlag‘
  5. centos7安装cassandra
  6. 20172331 《Java程序设计》第3周学习总结
  7. usb转rj45_超薄本也能有线上网,只需一个USB转网口小工具
  8. Publish over FTP发布报错
  9. 剑指offer(28)—数组中出现次数超过一半的数字
  10. NERO8.0刻录系统光盘
  11. 第二周教学课件及实验任务已发布!
  12. 正心,修身,方能齐家,治国,平天下
  13. Centos7快速搭建服务器加速
  14. 基于python实现的线性回归基础
  15. Kubernetes详解(四十一)——Secret创建
  16. win11使用win10右键菜单的方法
  17. Tushare的用法
  18. c++练习 日期的顺延显示
  19. Sublime Text教程
  20. SBAS-InSAR输出数据不正确的问题(2)

热门文章

  1. quartus+modelsim仿真教程
  2. 软件项目组织与管理期末考试复习要点整理翻译
  3. VBA编程教程(基础二)
  4. 89c51交通灯汇编语言程序,汇编语言的交通灯程序
  5. access 数据库入门
  6. HDU 2037 贪心
  7. 人月神话-软件开发现状
  8. MAPX中的数据绑定问题
  9. NetSpeedMonitor for mac
  10. 学习笔记-Speed-Win