上一篇文章介紹了驅動中minstrel_ht速率調整算法,atheros中提供了可選的的兩種速率調整算法,分別是ath9k和minstrel,這兩個算法分別位於:
drivers\net\wireless\ath\ath9k\rc.c···················Ath9k
net\mac80211\minstrel_ht.c···························Minstrel
無論從理論分析還是實驗結果上看,minstrel都要勝ath9k一籌,為了一個完整性,這里也把ath9k算法介紹一下,相比較於minstrel的隨機探測,ath9k是按照特定順序來探測的,不過這個順序的排列卻有問題。
1. 速率表
ath9k根據當前的標准是802.11a還是b/g/n,使用不同的速率表,這個速率表是硬編碼的,首先來看存儲這個速率表的結構體:
struct ath_rate_table { int rate_cnt; int mcs_start; struct { u16 rate_flags; u8 phy; u32 ratekbps; u32 user_ratekbps; u8 ratecode; u8 dot11rate; u8 ctrl_rate; u8 cw40index; u8 sgi_index; u8 ht_index; } info[RATE_TABLE_SIZE]; u32 probe_interval; u8 initial_ratemax; };
因為我的實驗環境用的是802.11n,所以使用的速率表是11na的:
static const struct ath_rate_table ar5416_11na_ratetable = { 68, 8, /* MCS start */ { [0] = { RC_L_SDT, WLAN_RC_PHY_OFDM, 6000, 5400, 0, 12, 0, 0, 0, 0 }, /* 6 Mb */ [1] = { RC_L_SDT, WLAN_RC_PHY_OFDM, 9000, 7800, 1, 18, 0, 1, 1, 1 }, /* 9 Mb */ [2] = { RC_L_SDT, WLAN_RC_PHY_OFDM, 12000, 10000, 2, 24, 2, 2, 2, 2 }, /* 12 Mb */ [3] = { RC_L_SDT, WLAN_RC_PHY_OFDM, 18000, 13900, 3, 36, 2, 3, 3, 3 }, /* 18 Mb */ [4] = { RC_L_SDT, WLAN_RC_PHY_OFDM, 24000, 17300, 4, 48, 4, 4, 4, 4 }, /* 24 Mb */ [5] = { RC_L_SDT, WLAN_RC_PHY_OFDM, 36000, 23000, 5, 72, 4, 5, 5, 5 }, /* 36 Mb */ [6] = { RC_L_SDT, WLAN_RC_PHY_OFDM, 48000, 27400, 6, 96, 4, 6, 6, 6 }, /* 48 Mb */ [7] = { RC_L_SDT, WLAN_RC_PHY_OFDM, 54000, 29300, 7, 108, 4, 7, 7, 7 }, /* 54 Mb */ [8] = { RC_HT_SDT_2040, WLAN_RC_PHY_HT_20_SS, 6500, 6400, 0, 0, 0, 38, 8, 38 }, /* 6.5 Mb */ …… [15] = { RC_HT_S_20, WLAN_RC_PHY_HT_20_SS, 65000, 59000, 7, 7, 4, 45, 16, 46 }, /* 65 Mb */ [16] = { RC_HT_S_20, WLAN_RC_PHY_HT_20_SS_HGI, 72200, 65400, 7, 7, 4, 45, 16, 46 }, /* 75 Mb */ …… [38] = { RC_HT_SDT_40, WLAN_RC_PHY_HT_40_SS, 13500, 13200, 0, 0, 0, 38, 38, 38 }, /* 13.5 Mb*/ …… [45] = { RC_HT_S_40, WLAN_RC_PHY_HT_40_SS, 135000, 112000, 7, 7, 4, 45, 46, 46 }, /* 135 Mb */ [46] = { RC_HT_S_40, WLAN_RC_PHY_HT_40_SS_HGI, 150000, 122000, 7, 7, 4, 45, 46, 46 }, /* 150 Mb */ …… [67] = { RC_HT_T_40, WLAN_RC_PHY_HT_40_TS_HGI, 450000, 346400, 23, 23, 4, 66, 67, 67 }, /* 450 Mb */ }, 50, /* probe interval */ WLAN_RC_HT_FLAG, /* Phy rates allowed initially */ };
這個變量的定義和前面的結構體一一對應,首先是rate_cnt,是68,即有68個速率,之后是mcs_start,也就是從第幾個速率開始是MCS的速率,對速率比較熟悉的能夠看出來,0-7分別是11b/g的8個傳統速率,最大54Mbps,從第8個開始,就是MCS速率,以[8]為例,結合前面的結構體看各個域的含義:
rate_flags | phy | ratekbps | user_ratekbps | ratecode | dot11rate | ctrl_rate | cw40index | sgi_index | ht_index |
RC_HT_SDT_2040 | WLAN_RC_PHY_HT_20_SS | 6500 | 6400 | 0 | 0 | 0 | 38 | 8 | 38 |
這里面字段比較多,不過很多我都沒有深究,一知半解,rate_flags的含義結合rc.h中的宏定義更清晰一些,RC_HT_S(單流)D(雙流)T(三流)_2040(20/40M帶寬),物理意義我不是很清楚,可能是說這個參數可以在哪些配置下用吧,比如說在配置的40MHz帶寬下,是可以使用20MHz的MCS0的,但是配置的20MHz帶寬就不能用40Mhz的MCS0發送。phy是當前速率所對應的物理層參數,這個就是20MHz的SS(單流),數據率是6.5Mbps,user_ratekbps我沒有仔細看過,可能是對去掉MAC和PHY頭部之后對速率的估算吧,ratecode和dot11rate說的基本是一個事,就是說這是哪個MCS,這里就是MCS0,ctrl_rate是說,在11b/g的速率里面,哪一個可以作為當前速率的保底速率,最后三個就是說,當前的速率,也就是20MHz的MCS0,在40MHz下對應速率表里的第幾個速率、SGI的情況下對應於哪一個,40MHz+SGI的情況下又對應於第幾個速率。
介紹完速率表,剩下的,就按照和minstrel同樣的思路來分析,先來看注冊rate_control_ops的結構體:
static struct rate_control_ops ath_rate_ops = { .module = NULL, .name = "ath9k_rate_control", .tx_status = ath_tx_status, .get_rate = ath_get_rate, .rate_init = ath_rate_init, .rate_update = ath_rate_update, .alloc = ath_rate_alloc, .free = ath_rate_free, .alloc_sta = ath_rate_alloc_sta, .free_sta = ath_rate_free_sta, #ifdef CONFIG_ATH9K_DEBUGFS .add_sta_debugfs = ath_rate_add_sta_debugfs, #endif };
下面依次來看ath_rate_init、ath_get_rate、ath_tx_status。
2. 算法的初始化
for (i = 0; i < sband->n_bitrates; i++) { if (sta->supp_rates[sband->band] & BIT(i)) { ath_rc_priv->neg_rates.rs_rates[j] = (sband->bitrates[i].bitrate * 2) / 10; j++; } } ath_rc_priv->neg_rates.rs_nrates = j; if (sta->ht_cap.ht_supported) { for (i = 0, j = 0; i < 77; i++) { if (sta->ht_cap.mcs.rx_mask[i/8] & (1<<(i%8))) ath_rc_priv->neg_ht_rates.rs_rates[j++] = i; if (j == ATH_RATE_MAX) break; } ath_rc_priv->neg_ht_rates.rs_nrates = j; }
這兩個循環的代碼不用深究,n_bitrates一般是8,第一個循環就是看看11b/g的那8個速率哪一個被當前的驅動和硬件支持,第二個循環就是看看MCS的速率又有哪些被支持,把能支持的加入到ath_rc_priv->neg_rates和ath_rc_priv->neg_ht_rates中,neg表示Negotatied。
下面主要做了三件事:1.判斷當前的配置是20MHz還是40MHz,2.當前配置是不是SGI,3.根據配置選擇速率表,是前面介紹的ar5416_11na_ratetable,還是ar5416_11ng_ratetable、ar5416_11a_ratetable、ar5416_11g_ratetable。
最后調用了ath_rc_init函數,這個函數對速率調整各個速率的基本參數進行初始化:
for (i = 0 ; i < ath_rc_priv->rate_table_size; i++) { ath_rc_priv->per[i] = 0; }
有這么一個叫per的數組,per是packet error rate的縮寫,數組的每一項對應於剛才的那個速率表,這里先對所有的速率的per初始化為0。之后初始化兩個變量:
for (i = 0; i < WLAN_RC_PHY_MAX; i++) { for (j = 0; j < MAX_TX_RATE_PHY; j++) ath_rc_priv->valid_phy_rateidx[i][j] = 0; ath_rc_priv->valid_phy_ratecnt[i] = 0; }
打眼一看這兩個變量不是很好理解,但是只要一看WLAN_RC_PHY_MAX是什么,就比較明朗了:
enum { WLAN_RC_PHY_OFDM, WLAN_RC_PHY_CCK, WLAN_RC_PHY_HT_20_SS, WLAN_RC_PHY_HT_20_DS, WLAN_RC_PHY_HT_20_TS, WLAN_RC_PHY_HT_40_SS, WLAN_RC_PHY_HT_40_DS, WLAN_RC_PHY_HT_40_TS, WLAN_RC_PHY_HT_20_SS_HGI, WLAN_RC_PHY_HT_20_DS_HGI, WLAN_RC_PHY_HT_20_TS_HGI, WLAN_RC_PHY_HT_40_SS_HGI, WLAN_RC_PHY_HT_40_DS_HGI, WLAN_RC_PHY_HT_40_TS_HGI, WLAN_RC_PHY_MAX };
前面的那個速率表如果看熟了,這些東西就會變得非常眼熟,正是每個速率的第二個域(也就是前面說明速率表結構體的那個表格的第二列),phy。換句話說,ath9k在這個時候也是給這些速率分了個組,分組的依據就是每個速率的phy域,valid_phy_ratecnt是存儲這個組里有幾個速率,valid_phy_rateidx就是存儲各個組里都有哪些速率了,后面的一段代碼就不粘帖了,就是把速率表里的速率遍歷一下,把被支持的速率挑出來,給這兩個數組賦上正確的值。這時候valid_phy_rateidx就是一個存儲被支持的速率的二維數組了,這里要是不能理解也沒有關系,因為這個函數之后基本就用不到這兩個變量了,下一步,把這個二維數組壓成一個一維數組,存儲在valid_rate_index里面,只要明白valid_rate_index是什么意思就夠了,結合一下代碼看看把二維數組壓成一維數組是什么意思(看代碼應該比較清晰):
for (i = 0, k = 0; i < WLAN_RC_PHY_MAX; i++) { for (j = 0; j < ath_rc_priv->valid_phy_ratecnt[i]; j++) { ath_rc_priv->valid_rate_index[k++] = ath_rc_priv->valid_phy_rateidx[i][j]; } …… }
然后,按照速率值的大小,也就是那個多少Mbps,對這些速率排序(也就是對ath_rc_priv->valid_rate_index這個數組排序):
ath_rc_sort_validrates(rate_table, ath_rc_priv);
這個數組,就決定了以后的探測順序,Ath9k的探測就是沿着這個方向,碰到投遞率低的速率就停止探測,但是因為有帶寬和空間流的影響,120Mbps的速率投遞率是10%,150Mbps的投遞率就一定很低嗎?這是不一定的。
2. 獲取發送速率
一開始,和minstrel一樣,當目標站點不存在,或者本次發送不需要等ACK的時候,為了確保數據包盡可能被對方正確接收,那么會直接用傳統速率來發送,不給它分配MCS速率:
if (rate_control_send_low(sta, priv_sta, txrc)) return;
接下來調用ath_rc_get_highest_rix函數,計算當前哪個速率是最好的速率:
maxindex = ath_rc_priv->max_valid_rate-1; minindex = 0; best_rate = minindex; for (index = maxindex; index >= minindex ; index--) { u8 per_thres; rate = ath_rc_priv->valid_rate_index[index]; …… per_thres = ath_rc_priv->per[rate]; if (per_thres < 12) per_thres = 12; this_thruput = rate_table->info[rate].user_ratekbps * (100 - per_thres); if (best_thruput <= this_thruput) { best_thruput = this_thruput; best_rate = rate; } }
從maxindex開始往minindex遍歷,是在信道狀況好的情況下,可以減少內存計算開銷,當速率的per小於12%的時候,就按12%來對待,這個在源碼里有解釋:For TCP the average collision rate is around 11%, so we ignore PERs less than this. This is to prevent the rate we are currently using (whose PER might be in the 10-15 range because of TCP collisions) looking worse than the next lower rate whose PER has decayed close to 0. If we used to next lower rate, its PER would grow to 10-15 and we would be worse off then staying at the current rate.
調用ath_rc_get_highest_rix找到最好的速率之后存到變量rix中,rix是前面講過的11na速率表中的速率索引。還有一個比較關鍵的函數是ath_rc_get_lower_rix,這個函數是要獲取比rix稍差的速率,獲取思路很簡單,還記得前面的valid_rate_index嗎,這個數組里存儲着按比特率排序的各個速率,次佳速率就是這個數組中某速率的前一個(這個取法當然是不怎么科學的)。算法維護了一個當前最大速率的序號,如果新獲得的這個highest_rix比這個數大,並且離上次探測時間超過一定閾值,就會啟動探測,這個最后會講。
if (is_probe) { ath_rc_rate_set_series(rate_table, &rates[i++], txrc, 1, rix, 0); ath_rc_get_lower_rix(rate_table, ath_rc_priv, rix, &rix); ath_rc_rate_set_series(rate_table, &rates[i++], txrc, try_per_rate, rix, 0); tx_info->flags |= IEEE80211_TX_CTL_RATE_CTRL_PROBE; } else { ath_rc_rate_set_series(rate_table, &rates[i++], txrc, try_per_rate, rix, 0); } for ( ; i < 3; i++) { ath_rc_get_lower_rix(rate_table, ath_rc_priv, rix, &rix); ath_rc_rate_set_series(rate_table, &rates[i], txrc, try_per_rate, rix, 1); } try_per_rate = 8; if ((rates[2].flags & IEEE80211_TX_RC_MCS) && (!(tx_info->flags & IEEE80211_TX_CTL_AMPDU) || (ath_rc_priv->per[high_rix] > 45))) rix = ath_rc_get_highest_rix(sc, ath_rc_priv, rate_table, &is_probe, true); else ath_rc_get_lower_rix(rate_table, ath_rc_priv, rix, &rix); ath_rc_rate_set_series(rate_table, &rates[i], txrc, try_per_rate, rix, 1);
和minstrel類似,關鍵函數長得也很像,標紅的三個參數,分別是底層發包需要的速率結構體、最大發送次數和速率編號,ath9k是用4個速率來發送,對應的參數要賦到rates[0]、rates[1]、rates[2]和rates[3]這四個變量上,try_per_rate初始為4,這段代碼的含義就是:如果需要探測,則rates[0]用探測速率發1次,如果失敗,則換rates[1]用探測速率的次佳速率發4次,再失敗就換rates[2]用rates[1]的次佳速率再發4次。如果不是探測,就按當前最佳速率、次佳速率、次次佳速率的順序各嘗試4次。最后還剩一個rates[3],如果rates[0]的per超過45%,重新獲取最高速率作為rates[3]的發送速率,如果rates[0]的per不到45%,就用rates[2]的次佳速率發送,最大嘗試8次。在2.4GHz環境下或者數據包分片的情況下還會再微調,此處不再分析。
3. 發送完成后更新速率狀態
和minstrel一樣,聚合幀發送完之后,每一個子幀都會調用速率調整算法的tx_status函數,但是每個聚合幀里只有一個幀是攜帶了有用信息的,其他幀直接返回不予處理:
if ((tx_info->flags & IEEE80211_TX_CTL_AMPDU) && !(tx_info->flags & IEEE80211_TX_STAT_AMPDU)) return;
對於攜帶了發送狀況信息的幀,驅動用了一個叫做xretries的變量區分這4個速率的狀態,現在考慮兩個例子:
例1:rates[0]發送4次都失敗,rates[1]發送4次也都失敗,rates[2]發送第3次成功,那么使用了的速率就是3個,rates[3]沒有用到;
例2:rates[0]-rates[3]這總共20次發送全部失敗。
xretries | 含義 |
0 | 數據幀最終被成功發送,並且是使用的本速率,對上面的例子來說,例1的rates[2]的xretries就是0 |
1 | 數據幀最終沒有成功發送,丟了,例2的4個速率的xretries都是1 |
2 | 數據幀最終被成功發送,但不是使用的本速率,例1的rates[0]和rates[1]的xretries是2 |
針對每個速率,首先調用ath_rc_update_per來更新per:
if (xretries == 1) { ath_rc_priv->per[tx_rate] += 30; if (ath_rc_priv->per[tx_rate] > 100) ath_rc_priv->per[tx_rate] = 100; }
如果是xretries=1這種情況,per直接加30%。
else { /* xretries == 2 *//* new_PER = 7/8*old_PER + 1/8*(currentPER) */ ath_rc_priv->per[tx_rate] = (u8)(last_per - (last_per >> 3) + (100 >> 3)); }
新的per是舊per的7/8加上本次per的1/8,因為xretries=2對應的是發送全都失敗的速率,所以本次per就是100%。
最后也是最復雜的就是最后成功的那個速率,這里需要先介紹幾個基本參數,從頭來看一下這個函數的參數和一個寫死的lookup數組:
static void ath_rc_update_per(struct ath_softc *sc, const struct ath_rate_table *rate_table, struct ath_rate_priv *ath_rc_priv, struct ieee80211_tx_info *tx_info, int tx_rate, int xretries, int retries, u32 now_msec) { int count, n_bad_frames; u8 last_per; static const u32 nretry_to_per_lookup[10] = { 100 * 0 / 1, 100 * 1 / 4, 100 * 1 / 2, 100 * 3 / 4, 100 * 4 / 5, 100 * 5 / 6, 100 * 6 / 7, 100 * 7 / 8, 100 * 8 / 9, 100 * 9 / 10 };
tx_rate不用說就能猜到,表示當前速率,xretries前面已經介紹,現在介紹的這種情況xretries=0。retries是說這個速率重傳了多少次,比如rates[0]發了4次都失敗了,就是重傳了3次,rates[2]第3次成功,就是retries=2。
下面看邏輯:
if (n_bad_frames) { if (tx_info->status.ampdu_len > 0) { int n_frames, n_bad_tries; u8 cur_per, new_per; n_bad_tries = retries * tx_info->status.ampdu_len + n_bad_frames; n_frames = tx_info->status.ampdu_len * (retries + 1); cur_per = (100 * n_bad_tries / n_frames) >> 3; new_per = (u8)(last_per - (last_per >> 3) + cur_per); ath_rc_priv->per[tx_rate] = new_per; } } else { ath_rc_priv->per[tx_rate] = (u8)(last_per - (last_per >> 3) + (nretry_to_per_lookup[retries] >> 3)); }
這里又分了兩種情況,前面說的成功發送其實是指聚合幀被成功發送,成功與否的標志是收到了塊確認,也就是說有可能有子幀因為CRC等錯誤還是沒有被正確接受,這樣出錯的幀的數量就是n_bad_frames,更新PER的算法都是一樣的:new_per=old_per*7/8+cur_per/8,但是cur_per的算法不固定,如果n_bad_frames是0,那不按照正統方法來算,還是假設第3次發送成功,也就是重傳了兩次,那么this_per不是常規認為的66%,而是查表得來的50%。如果n_bad_frames不是0,那么就是正規方法了,前面retries次一共丟了多少加上最后一次丟了幾個,除以總的發送子幀數就是cur_per。
最后,如果成功發送的速率是探測速率,如果它的PER小於50%且大於30%的話,置成20%,並且,因為這次探測的結果還不錯,所以下次探測的時間就會在半個探測間隔(rate_table->probe_interval / 2)之后就開始:
if (ath_rc_priv->probe_rate && ath_rc_priv->probe_rate == tx_rate) { if (retries > 0 || 2 * n_bad_frames > tx_info->status.ampdu_len) { …… } else { …… if (ath_rc_priv->per[probe_rate] > 30) ath_rc_priv->per[probe_rate] = 20; …… ath_rc_priv->probe_time = now_msec - rate_table->probe_interval / 2; } }
后面還有一些小判斷,如果當前速率的PER已經超過55%,而且這是一個比當前最高速率更小的速率,按照ath9k算法的理念,它按比特率排序的結果就是越靠前的速率越穩定,如果當前速率都已經不行了,那么比它更靠后的最大速率肯定更不行了,這時候就降低rate_max_phy的值:
if (ath_rc_priv->per[tx_rate] >= 55 && tx_rate > 0 && rate_table->info[tx_rate].ratekbps <= rate_table->info[ath_rc_priv->rate_max_phy].ratekbps) { ath_rc_get_lower_rix(rate_table, ath_rc_priv, (u8)tx_rate, &ath_rc_priv->rate_max_phy); /* Don't probe for a little while. */ ath_rc_priv->probe_time = now_msec; }
ath9k是以排序越靠前的速率越穩定為前提的,那么如果這個單調性不存在了怎么辦?ath9k的做法是強制讓它單調,如果前面的速率的PER比后面的大,就賦值成和后面一樣的:
if (ath_rc_priv->per[tx_rate] < last_per) { for (rate = tx_rate - 1; rate >= 0; rate--) { if (ath_rc_priv->per[rate] > ath_rc_priv->per[rate+1]) { ath_rc_priv->per[rate] = ath_rc_priv->per[rate+1]; } } }
同樣,從當前速率越往后就需要越不穩定,也需要強制規范一下,這個代碼就不貼了。
最后,為了不讓PER升的太高,每隔一段時間就會降為原來的7/8:
if (now_msec - ath_rc_priv->per_down_time >= rate_table->probe_interval) { for (rate = 0; rate < size; rate++) { ath_rc_priv->per[rate] = 7 * ath_rc_priv->per[rate] / 8; } ath_rc_priv->per_down_time = now_msec; }
4. 探測頻率
最后一個問題,ath9k什么時候開始探測,本文開頭介紹的rate_table里有一個域是probe_interval,11na的速率表設定的值是50,再加上ath_rc_priv->probe_time存儲上次探測的時間,根據這兩個變量,在時間上控制探測的間隔。除此之外還有一個非常重要的變量,沿着這個變量的賦值搜索下去,就能弄清楚探測的原理,這個變量就是ath_rc_priv->rate_max_phy,前面反復提到,ath9k對所有的速率排了個序,它認為,這些速率的表現是單調變化的,如果排序之后的速率n已經不行了,那么n+1、n+2肯定都已經不行了,這個rate_max_phy就是當前性能不錯的最大速率的序號,下面就沿着這個這個變量分析:
在ath_rc_init函數中完成對所有速率的排序之后,將排序后的倒數第三個速率設置為rate_max_phy:
ath_rc_priv->rate_max_phy = ath_rc_priv->valid_rate_index[k-4];
此時k=valid_rate_index.length,為什么是取倒數第三個作為rate_max_phy,不了解。
前面介紹,在發送數據包時,會尋找當前吞吐率最高的速率,找到之后會進行下面的判斷:
if (rate >= ath_rc_priv->rate_max_phy) { rate = ath_rc_priv->rate_max_phy; /* Probe the next allowed phy state */ if (ath_rc_get_nextvalid_txrate(rate_table, ath_rc_priv, rate, &next_rate) && (now_msec - ath_rc_priv->probe_time > rate_table->probe_interval) && (ath_rc_priv->hw_maxretry_pktcnt >= 1)) { rate = next_rate; ath_rc_priv->probe_rate = rate; ath_rc_priv->probe_time = now_msec; ath_rc_priv->hw_maxretry_pktcnt = 0; *is_probing = 1; } }
rate就是找到的最大速率,仔細看代碼會發現,這個rate其實一定會是一個小於等於rate_max_phy的數,因為算法自動忽略了大於rate_max_phy的速率,不過沒有關系,對這段代碼的解釋可以是這樣:當找到的最佳速率不比之前設定的最大速率小的時候,說明這個時候可以往高速率上探測一下了,於是呢get_nextvalid_txrate獲取rate之后更高的速率,並且判斷一下是不是已經到了該探測的時間了,如果是,則使用這個更高速率進行探測。最后,這個rate_max_phy是怎么改變的呢,當探測幀的投遞率大於50%的時候,會把剛剛探測的速率作為新的rate_max_phy:
if (ath_rc_priv->probe_rate && ath_rc_priv->probe_rate == tx_rate) { if (retries > 0 || 2 * n_bad_frames > tx_info->status.ampdu_len) { ath_rc_priv->probe_rate = 0; } else { u8 probe_rate = 0; ath_rc_priv->rate_max_phy = ath_rc_priv->probe_rate; probe_rate = ath_rc_priv->probe_rate; …… ath_rc_priv->probe_rate = 0; /* * Since this probe succeeded, we allow the next probe twice as soon.
* This allows the maxRate to move up faster if the probes are successful. */ ath_rc_priv->probe_time = now_msec - rate_table->probe_interval / 2; } }
至此,ath9k速率調整算法的基本流程就差不多了。