前面的六篇文章,我们已经讨论了dapm关于动态电源管理的有关知识,包括widget的创建和初始化,widget之间的连接以及widget的上下电顺序等等。本章我们准备讨论dapm框架中的另一个机制:事件机制。通过dapm事件机制,widget可以对它所关心的dapm事件做出反应,这种机制对于扩充widget的能力非常有用,例如,对于那些位于codec之外的widget,好像喇叭功放、外部的前置放大器等等,由于不是使用codec内部的寄存器进行电源控制,我们就必须利用dapm的事件机制,获得相应的上下电事件,从而可以定制widget自身的电源控制功能。

/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/

dapm event的种类


dapm目前为我们定义了9种dapm event,他们分别是:

事件类型 说明
SND_SOC_DAPM_PRE_PMU widget要上电前发出的事件
SND_SOC_DAPM_POST_PMU widget要上电后发出的事件
SND_SOC_DAPM_PRE_PMD widget要下电前发出的事件
SND_SOC_DAPM_POST_PMD widget要下电后发出的事件
SND_SOC_DAPM_PRE_REG 音频路径设置之前发出的事件
SND_SOC_DAPM_POST_REG 音频路径设置之后发出的事件
SND_SOC_DAPM_WILL_PMU 在处理up_list链表之前发出的事件
SND_SOC_DAPM_WILL_PMD 在处理down_list链表之前发出的事件
SND_SOC_DAPM_PRE_POST_PMD SND_SOC_DAPM_PRE_PMD和
SND_SOC_DAPM_POST_PMD的合并

前8种每种占据一个位,所以,我们可以在一个整数中表达多个我们需要关心的dapm事件,只要把它们按位或进行合并即可。

widget的event回调函数


ALSA声卡驱动中的DAPM详解之二:widget-具备路径和电源管理信息的kcontrol中,我们已经介绍过代表widget的snd_soc_widget结构,在这个结构体中,有一个event字段用于保存该widget的事件回调函数,同时,event_flags字段用于保存该widget需要关心的dapm事件种类,只有event_flags字段中相应的事件位被设置了的事件才会发到event回调函数中进行处理。

我们知道,dapm为我们提供了常用widget的定义辅助宏,使用以下这几种辅助宏定义widget时,默认需要我们提供dapm event回调函数

  • SND_SOC_DAPM_MIC
  • SND_SOC_DAPM_HP
  • SND_SOC_DAPM_SPK
  • SND_SOC_DAPM_LINE
这些widget都是位于codec外部的器件,它们无法使用通用的寄存器操作来控制widget的电源状态,所以需要我们提供event回调函数。以下的例子来自dapm的内核文档,外部的喇叭功放通过CORGI_GPIO_APM_ON这个gpio来控制它的电源状态:
  1. /* turn speaker amplifier on/off depending on use */
  2. static int corgi_amp_event(struct snd_soc_dapm_widget *w, int event)
  3. {
  4. gpio_set_value(CORGI_GPIO_APM_ON, SND_SOC_DAPM_EVENT_ON(event));
  5. return 0;
  6. }
  7. /* corgi machine dapm widgets */
  8. static const struct snd_soc_dapm_widget wm8731_dapm_widgets =
  9. SND_SOC_DAPM_SPK("Ext Spk", corgi_amp_event);

另外,我们也可以通过以下这些带"_E"后缀的辅助宏版本来定义需要dapm事件的widget:

  • SND_SOC_DAPM_PGA_E
  • SND_SOC_DAPM_OUT_DRV_E
  • SND_SOC_DAPM_MIXER_E
  • SND_SOC_DAPM_MIXER_NAMED_CTL_E
  • SND_SOC_DAPM_SWITCH_E
  • SND_SOC_DAPM_MUX_E
  • SND_SOC_DAPM_VIRT_MUX_E

触发dapm event


我们已经定义好了带有event回调的widget,那么,在那里触发这些dapm event?答案是:在dapm_power_widgets函数的处理过程中,dapm_power_widgets函数我们已经在ALSA声卡驱动中的DAPM详解之六:精髓所在,牵一发而动全身中做了详细的分析,其中,在所有需要处理电源变化的widget被分别放入up_list和down_list链表后,会相应地发出各种dapm事件:

  1. static int dapm_power_widgets(struct snd_soc_card *card, int event)
  2. {
  3. ......
  4. list_for_each_entry(w, &down_list, power_list) {
  5. dapm_seq_check_event(card, w, SND_SOC_DAPM_WILL_PMD);
  6. }
  7. list_for_each_entry(w, &up_list, power_list) {
  8. dapm_seq_check_event(card, w, SND_SOC_DAPM_WILL_PMU);
  9. }
  10. /* Power down widgets first; try to avoid amplifying pops. */
  11. dapm_seq_run(card, &down_list, event, false);
  12. dapm_widget_update(card);
  13. /* Now power up. */
  14. dapm_seq_run(card, &up_list, event, true);
  15. ......
  16. }

可见,在真正地进行上电和下电之前,dapm向down_list链表中的每个widget发出SND_SOC_DAPM_WILL_PMD事件,而向up_list链表中的每个widget发出SND_SOC_DAPM_WILL_PMU事件。在处理上下电的函数dapm_seq_run中,会调用dapm_seq_run_coalesced函数执行真正的寄存器操作,进行widget的电源控制,dapm_seq_run_coalesced也会发出另外几种dapm事件:

  1. static void dapm_seq_run_coalesced(struct snd_soc_card *card,
  2. struct list_head *pending)
  3. {
  4. ......
  5. list_for_each_entry(w, pending, power_list) {
  6. ......
  7. /* Check for events */
  8. dapm_seq_check_event(card, w, SND_SOC_DAPM_PRE_PMU);
  9. dapm_seq_check_event(card, w, SND_SOC_DAPM_PRE_PMD);
  10. }
  11. if (reg >= 0) {
  12. ......
  13. pop_wait(card->pop_time);
  14. soc_widget_update_bits_locked(w, reg, mask, value);
  15. }
  16. list_for_each_entry(w, pending, power_list) {
  17. dapm_seq_check_event(card, w, SND_SOC_DAPM_POST_PMU);
  18. dapm_seq_check_event(card, w, SND_SOC_DAPM_POST_PMD);
  19. }
  20. }

另外,负责更新音频路径的dapm_widget_update函数中也会发出dapm事件:

  1. static void dapm_widget_update(struct snd_soc_card *card)
  2. {
  3. struct snd_soc_dapm_update *update = card->update;
  4. struct snd_soc_dapm_widget_list *wlist;
  5. struct snd_soc_dapm_widget *w = NULL;
  6. unsigned int wi;
  7. int ret;
  8. if (!update || !dapm_kcontrol_is_powered(update->kcontrol))
  9. return;
  10. wlist = dapm_kcontrol_get_wlist(update->kcontrol);
  11. for (wi = 0; wi < wlist->num_widgets; wi++) {
  12. w = wlist->widgets[wi];
  13. if (w->event && (w->event_flags & SND_SOC_DAPM_PRE_REG)) {
  14. ret = w->event(w, update->kcontrol, SND_SOC_DAPM_PRE_REG);
  15. ......
  16. }
  17. }
  18. ......
  19. /* 更新kcontrol的值,改变音频路径 */
  20. ret = soc_widget_update_bits_locked(w, update->reg, update->mask,
  21. update->val);
  22. ......
  23. for (wi = 0; wi < wlist->num_widgets; wi++) {
  24. w = wlist->widgets[wi];
  25. if (w->event && (w->event_flags & SND_SOC_DAPM_POST_REG)) {
  26. ret = w->event(w, update->kcontrol, SND_SOC_DAPM_POST_REG);
  27. ......
  28. }
  29. }
  30. }

可见,改变路径的前后,分别发出了SND_SOC_DAPM_PRE_REG事件和SND_SOC_DAPM_POST_REG事件。

dai widget与stream widget


dai widget    在ALSA声卡驱动中的DAPM详解之四:在驱动程序中初始化并注册widget和route一文中,我们已经讨论过dai widget,dai widget又分为cpu dai widget和codec dai widget,它们在machine驱动分别匹配上相应的codec和platform后,由soc_probe_platform和soc_probe_codec这两个函数通过调用dapm的api函数:

  • snd_soc_dapm_new_dai_widgets
来创建的,通常会为playback和capture各自创建一个dai widget,他们的类型分别是:
  • snd_soc_dapm_dai_in      对应playback dai
  • snd_soc_dapm_dai_out    对应capture dai
另外,dai widget的名字是使用stream name来命名的,他通常来自snd_soc_dai_driver中的stream_name字段。dai widget的sname字段也使用同样的名字。
stream widget    stream widget通常是指那些要处理音频流数据的widget,它们包含以下这几种类型:
  • snd_soc_dapm_aif_in                 用SND_SOC_DAPM_AIF_IN辅助宏定义
  • snd_soc_dapm_aif_out               用SND_SOC_DAPM_AIF_OUT辅助宏定义
  • snd_soc_dapm_dac                    用SND_SOC_DAPM_AIF_DAC辅助宏定义
  • snd_soc_dapm_adc                    用SND_SOC_DAPM_AIF_ADC辅助宏定义
对于这几种widget,我们除了要指定widget的名字外,还要指定他对应的stream的名字,保存在widget的sname字段中。

连接dai widget和stream widget

默认情况下,驱动不会通过snd_soc_route来主动定义dai widget和stream widget之间的连接关系,实际上,他们之间的连接关系是由ASoc负责的,在声卡的初始化函数中,使用snd_soc_dapm_link_dai_widgets函数来建立他们之间的连接关系:
  1. static int snd_soc_instantiate_card(struct snd_soc_card *card)
  2. {
  3. ......
  4. /* card bind complete so register a sound card */
  5. ret = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
  6. card->owner, 0, &card->snd_card);
  7. ......
  8. if (card->dapm_widgets)
  9. snd_soc_dapm_new_controls(&card->dapm, card->dapm_widgets,
  10. card->num_dapm_widgets);
  11. /*  建立dai widget和stream widget之间的连接关系  */
  12. snd_soc_dapm_link_dai_widgets(card);
  13. ......
  14. if (card->controls)
  15. snd_soc_add_card_controls(card, card->controls, card->num_controls);
  16. ......
  17. if (card->dapm_routes)
  18. snd_soc_dapm_add_routes(&card->dapm, card->dapm_routes,
  19. card->num_dapm_routes);
  20. ......
  21. if (card->fully_routed)
  22. list_for_each_entry(codec, &card->codec_dev_list, card_list)
  23. snd_soc_dapm_auto_nc_codec_pins(codec);
  24. snd_soc_dapm_new_widgets(card);
  25. ret = snd_card_register(card->snd_card);
  26. ......
  27. return 0;
  28. }

我们再来分析一下snd_soc_dapm_link_dai_widgets函数,看看它是如何连接这两种widget的,它先是遍历声卡中所有的widget,找出类型为snd_soc_dapm_dai_in和snd_soc_dapm_dai_out的widget,通过widget的priv字段,取出widget对应的snd_soc_dai结构指针:

  1. int snd_soc_dapm_link_dai_widgets(struct snd_soc_card *card)
  2. {
  3. struct snd_soc_dapm_widget *dai_w, *w;
  4. struct snd_soc_dai *dai;
  5. /* For each DAI widget... */
  6. list_for_each_entry(dai_w, &card->widgets, list) {
  7. switch (dai_w->id) {
  8. case snd_soc_dapm_dai_in:
  9. case snd_soc_dapm_dai_out:
  10. break;
  11. default:
  12. continue;
  13. }
  14. dai = dai_w->priv;

接着,再次从头遍历声卡中所有的widget,找出能与dai widget相连接的stream widget,第一个前提条件是这两个widget必须位于同一个dapm context中:

  1. /* ...find all widgets with the same stream and link them */
  2. list_for_each_entry(w, &card->widgets, list) {
  3. if (w->dapm != dai_w->dapm)
  4. continue;

dai widget不会与dai widget相连,所以跳过它们:

  1. switch (w->id) {
  2. case snd_soc_dapm_dai_in:
  3. case snd_soc_dapm_dai_out:
  4. continue;
  5. default:
  6. break;
  7. }

dai widget的名字没有出现在要连接的widget的stream name中,跳过这个widget:

  1. if (!w->sname || !strstr(w->sname, dai_w->name))
  2. continue;

如果widget的stream name包含了dai的stream name,则匹配成功,连接这两个widget:

  1. if (dai->driver->playback.stream_name &&
  2. strstr(w->sname,
  3. dai->driver->playback.stream_name)) {
  4. dev_dbg(dai->dev, "%s -> %s\n",
  5. dai->playback_widget->name, w->name);
  6. snd_soc_dapm_add_path(w->dapm,
  7. dai->playback_widget, w, NULL, NULL);
  8. }
  9. if (dai->driver->capture.stream_name &&
  10. strstr(w->sname,
  11. dai->driver->capture.stream_name)) {
  12. dev_dbg(dai->dev, "%s -> %s\n",
  13. w->name, dai->capture_widget->name);
  14. snd_soc_dapm_add_path(w->dapm, w,
  15. dai->capture_widget, NULL, NULL);
  16. }
  17. }
  18. }
  19. return 0;

由此可见,dai widget和stream widget是通过stream name进行匹配的,所以,我们在定义codec的stream widget时,它们的stream name必须要包含dai的stream name,这样才能让ASoc自动把这两种widget连接在一起,只有把它们连接在一起,ASoc中的播放、录音和停止等事件,才能通过dai widget传递到codec中,使得codec中的widget能根据目前的播放状态,动态地开启或关闭音频路径上所有widget的电源。我们看看wm8993中的例子:

  1. SND_SOC_DAPM_AIF_OUT("AIFOUTL", "Capture", 0, SND_SOC_NOPM, 0, 0),
  2. SND_SOC_DAPM_AIF_OUT("AIFOUTR", "Capture", 1, SND_SOC_NOPM, 0, 0),
  3. SND_SOC_DAPM_AIF_IN("AIFINL", "Playback", 0, SND_SOC_NOPM, 0, 0),
  4. SND_SOC_DAPM_AIF_IN("AIFINR", "Playback", 1, SND_SOC_NOPM, 0, 0),

分别定义了左右声道两个stream name为Capture和Playback的stream widget。对应的dai driver结构定义如下:

  1. static struct snd_soc_dai_driver wm8993_dai = {
  2. .name = "wm8993-hifi",
  3. .playback = {
  4. .stream_name = "Playback",
  5. .channels_min = 1,
  6. .channels_max = 2,
  7. .rates = WM8993_RATES,
  8. .formats = WM8993_FORMATS,
  9. .sig_bits = 24,
  10. },
  11. .capture = {
  12. .stream_name = "Capture",
  13. .channels_min = 1,
  14. .channels_max = 2,
  15. .rates = WM8993_RATES,
  16. .formats = WM8993_FORMATS,
  17. .sig_bits = 24,
  18. },
  19. .ops = &wm8993_ops,
  20. .symmetric_rates = 1,
  21. };

可见,它们的stream name是一样的,声卡初始化阶段会把它们连接在一起。需要注意的是,如果我们定义了snd_soc_dapm_aif_in和snd_soc_dapm_aif_out类型的stream widget,并指定了他们的stream name,在定义DAC或ADC对应的widget时,它们的stream name最好不要也使用相同的名字,否则,dai widget即会连接上AIF,也会连接上DAC/ADC,造成音频路径的混乱:

  1. SND_SOC_DAPM_ADC("ADCL", NULL, WM8993_POWER_MANAGEMENT_2, 1, 0),
  2. SND_SOC_DAPM_ADC("ADCR", NULL, WM8993_POWER_MANAGEMENT_2, 0, 0),
  3. SND_SOC_DAPM_DAC("DACL", NULL, WM8993_POWER_MANAGEMENT_3, 1, 0),
  4. SND_SOC_DAPM_DAC("DACR", NULL, WM8993_POWER_MANAGEMENT_3, 0, 0),

stream event


把dai widget和stream widget连接在一起,就是为了能把ASoc中的pcm处理部分和dapm进行关联,pcm的处理过程中,会通过发出stream event来通知dapm系统,重新扫描并调整音频路径上各个widget的电源状态,目前dapm提供了以下几种stream event:

  1. /* dapm stream operations */
  2. #define SND_SOC_DAPM_STREAM_NOP                 0x0
  3. #define SND_SOC_DAPM_STREAM_START               0x1
  4. #define SND_SOC_DAPM_STREAM_STOP                0x2
  5. #define SND_SOC_DAPM_STREAM_SUSPEND             0x4
  6. #define SND_SOC_DAPM_STREAM_RESUME              0x8
  7. #define SND_SOC_DAPM_STREAM_PAUSE_PUSH  0x10
  8. #define SND_SOC_DAPM_STREAM_PAUSE_RELEASE       0x20

比如,在soc_pcm_prepare函数中,会发出SND_SOC_DAPM_STREAM_START事件:

  1. snd_soc_dapm_stream_event(rtd, substream->stream,
  2. SND_SOC_DAPM_STREAM_START);

而在soc_pcm_close函数中,会发出SND_SOC_DAPM_STREAM_STOP事件:

  1. snd_soc_dapm_stream_event(rtd,
  2. SNDRV_PCM_STREAM_PLAYBACK,
  3. SND_SOC_DAPM_STREAM_STOP);

snd_soc_dapm_stream_event函数最终会使用soc_dapm_stream_event函数来完成具体的工作:

  1. static void soc_dapm_stream_event(struct snd_soc_pcm_runtime *rtd, int stream,
  2. int event)
  3. {
  4. struct snd_soc_dapm_widget *w_cpu, *w_codec;
  5. struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
  6. struct snd_soc_dai *codec_dai = rtd->codec_dai;
  7. if (stream == SNDRV_PCM_STREAM_PLAYBACK) {
  8. w_cpu = cpu_dai->playback_widget;
  9. w_codec = codec_dai->playback_widget;
  10. } else {
  11. w_cpu = cpu_dai->capture_widget;
  12. w_codec = codec_dai->capture_widget;
  13. }

该函数首先从snd_soc_pcm_runtime结构中取出cpu dai widget和codec dai widget,接下来:

  1. if (w_cpu) {
  2. dapm_mark_dirty(w_cpu, "stream event");
  3. switch (event) {
  4. case SND_SOC_DAPM_STREAM_START:
  5. w_cpu->active = 1;
  6. break;
  7. case SND_SOC_DAPM_STREAM_STOP:
  8. w_cpu->active = 0;
  9. break;
  10. case SND_SOC_DAPM_STREAM_SUSPEND:
  11. case SND_SOC_DAPM_STREAM_RESUME:
  12. case SND_SOC_DAPM_STREAM_PAUSE_PUSH:
  13. case SND_SOC_DAPM_STREAM_PAUSE_RELEASE:
  14. break;
  15. }
  16. }

把cpu dai widget加入到dapm_dirty链表中,根据stream event的类型,把cpu dai widget设定为激活状态或非激活状态,接下来,对codec dai widget做出同样的处理:

  1. if (w_codec) {
  2. dapm_mark_dirty(w_codec, "stream event");
  3. switch (event) {
  4. case SND_SOC_DAPM_STREAM_START:
  5. w_codec->active = 1;
  6. break;
  7. case SND_SOC_DAPM_STREAM_STOP:
  8. w_codec->active = 0;
  9. break;
  10. case SND_SOC_DAPM_STREAM_SUSPEND:
  11. case SND_SOC_DAPM_STREAM_RESUME:
  12. case SND_SOC_DAPM_STREAM_PAUSE_PUSH:
  13. case SND_SOC_DAPM_STREAM_PAUSE_RELEASE:
  14. break;
  15. }
  16. }

最后,它调用了我们熟悉的dapm_power_widgets函数:

  1. dapm_power_widgets(rtd->card, event);

因为dai widget和codec上的stream widget是相连的,所以,dai widget的激活状态改变,会沿着音频路径传递到路径上的所有widget,等dapm_power_widgets返回后,如果发出的是SND_SOC_DAPM_STREAM_START事件,路径上的所有widget会处于上电状态,保证音频数据流的顺利播放,如果发出的是SND_SOC_DAPM_STREAM_STOP事件,路径上的所有widget会处于下电状态,保证最小的功耗水平。

ALSA声卡驱动中的DAPM详解之七:dapm事件机制(dapm event)的更多相关文章

  1. ALSA声卡驱动中的DAPM详解之六:精髓所在,牵一发而动全身

    设计dapm的主要目的之一,就是希望声卡上的各种部件的电源按需分配,需要的就上电,不需要的就下电,使得整个音频系统总是处于最小的耗电状态,最主要的就是,这一切对用户空间的应用程序是透明的,也就是说,用 ...

  2. ALSA声卡驱动中的DAPM详解之四:在驱动程序中初始化并注册widget和route

    前几篇文章我们从dapm的数据结构入手,了解了代表音频控件的widget,代表连接路径的route以及用于连接两个widget的path.之前都是一些概念的讲解以及对数据结构中各个字段的说明,从本章开 ...

  3. ALSA声卡驱动中的DAPM详解之三:如何定义各种widget

    上一节中,介绍了DAPM框架中几个重要的数据结构:snd_soc_dapm_widget,snd_soc_dapm_path,snd_soc_dapm_route.其中snd_soc_dapm_pat ...

  4. ALSA声卡驱动中的DAPM详解之五:建立widget之间的连接关系

    前面我们主要着重于codec.platform.machine驱动程序中如何使用和建立dapm所需要的widget,route,这些是音频驱动开发人员必须要了解的内容,经过前几章的介绍,我们应该知道如 ...

  5. ALSA声卡驱动中的DAPM详解之一:kcontrol

    DAPM是Dynamic Audio Power Management的缩写,直译过来就是动态音频电源管理的意思,DAPM是为了使基于linux的移动设备上的音频子系统,在任何时候都工作在最小功耗状态 ...

  6. ALSA声卡驱动中的DAPM详解之二:widget-具备路径和电源管理信息的kcontrol

    上一篇文章中,我们介绍了音频驱动中对基本控制单元的封装:kcontrol.利用kcontrol,我们可以完成对音频系统中的mixer,mux,音量控制,音效控制,以及各种开关量的控制,通过对各种kco ...

  7. Linux ALSA声卡驱动之八:ASoC架构中的Platform

    1.  Platform驱动在ASoC中的作用 前面几章内容已经说过,ASoC被分为Machine,Platform和Codec三大部件,Platform驱动的主要作用是完成音频数据的管理,最终通过C ...

  8. Linux驱动开发必看详解神秘内核(完全转载)

    Linux驱动开发必看详解神秘内核 完全转载-链接:http://blog.chinaunix.net/uid-21356596-id-1827434.html   IT168 技术文档]在开始步入L ...

  9. Maven依赖中的scope详解,在eclipse里面用maven install可以编程成功,到服务器上用命令执行报VM crash错误

    Maven依赖中的scope详解 项目中用了<scope>test</scope>在eclipse里面用maven install可以编译成功,到服务器上用命令执行报VM cr ...

随机推荐

  1. dutacm.club_1085_Water Problem_(矩阵快速幂)

    1085: Water Problem Time Limit:3000/1000 MS (Java/Others)   Memory Limit:163840/131072 KB (Java/Othe ...

  2. Android(java)学习笔记204:JNI之native方法头文件的生成

    1. JDK1.6 ,进入到工程的bin目录下classes目录下: 使用命令: javah  packageName.ClassName 会在当前目录下生成头文件,从头文件找到jni协议方法 下面举 ...

  3. TCP端口状态LISTENING ESTABLISHED CLOSE_WAIT TIME_WAIT SYN_SENT

    TCP状态转移要点 TCP协议规定,对于已经建立的连接,网络双方要进行四次握手才能成功断开连接,如果缺少了其中某个步骤,将会使连接处于假死状态,连接本身占用的资源不 会被释放.网络服务器程序要同时管理 ...

  4. centos7服务器安装fail2ban配合Firewalld防护墙防止SSH爆破与防护网站CC攻击

    centos7服务器安装fail2ban配合Firewalld防护墙防止SSH爆破与防护网站CC攻击 1.检查firewalld是否启用 #如果您已经安装iptables建议先关闭 service i ...

  5. LeetCode141LinkedListCycle和142LinkedListCycleII

    141题:判断链表是不是存在环! // 不能使用额外的存储空间 public boolean hasCycle(ListNode head) { // 如果存在环的 两个指针用不一样的速度 会相遇 L ...

  6. Docker从入门到实践

    一般说来 SPA 的项目我们只要启一个静态文件 Server 就可以了,但是针对传统项目就不一样了,一个项目会依赖很多服务端程序.之前我们的开发模式是在一台开发机上部署开发环境,所有人都在这台开发机上 ...

  7. Django-REST-Framework JWT 实现SSO认证(下)

    在上一篇博客中,我已经对JSON Web 认证做了简单的解释,这篇博客是续篇,若不了解,请看上一篇博客:https://www.cnblogs.com/yushenglin/p/10863184.ht ...

  8. [bzoj4247][挂饰] (动规+排序)

    Description JOI君有N个装在手机上的挂饰,编号为1...N. JOI君可以将其中的一些装在手机上. JOI君的挂饰有一些与众不同——其中的一些挂饰附有可以挂其他挂件的挂钩.每个挂件要么直 ...

  9. Maven学习总结(30)——Maven项目通用三级版本号说明

     项目版本号说明     当前版本号:1.0.0-SNAPSHOT     本项目采用通用的三级版本号,版本号格式是[主版本号].[副版本号].[修复版本号]-[稳定状态],如:1.0.0-SNAPS ...

  10. 添物零基础到大型全栈架构师 不花钱学计算机及编程(预备篇)— C语言编程基础

    ​C语言介绍 C语言基本是每个编程人员必学的一面语言,很好掌握,是理解编程的关键.很多编程语言基于其编写或者基于此语言的衍生品编写. C语言是人机交互的一个基础语言之一,虽然是之一,单一般其实就是唯一 ...