WiFi995 的程式架構是並不複雜,但行數並不少,所有的功能大概有 600 行。其中包含信標用的程式、搜尋模式的程式、還有一些設定的程式碼。所以解說上可能會有點太漫長,會儘量挑重點講,一些太直覺的程式碼用途,就不再贅述。
程式流程
ESP8266 的使用者程式,最後都是呼叫到 user_init() @ user_main.c 的這支程式開始。而WiFi995 的程式流程,我們先稍微解說一下,再來看細部的程式碼。
- user_init() 裡,會先判斷是否是第一次上電。在信標模式下,進省電模式再醒來,基本上就是類似重上電的行為。差別在於 RTC 沒重上電,ESP8266 就是靠它來重新啟用。因此,每次醒來後,軟體的所有的變數都變成預設值。好在 RTC 內部有一小塊的記憶體可以供使用者使用,因此我們可以將一些狀態存在 RTC Memory 裡。在這裡,我們只是單純的在第一次開機會發出 BEEP 的聲音而已,用以通知硬體已準備完成。
- 接下來會判斷是否為信標模式或搜尋模式。搜尋模式每300ms, 會執行的 tm_stafunc() 這個 function, 做一些檢查或亮燈的動作,最重要是還會進入 Monitor Mode,來接收所有的 WiFi 資料,並註冊其處理 function 為 promisc_cb()。而在收到 WiFi 資料時,系統就會呼叫 promisc_cb(),判斷是否收到我們想要的信標訊號,是的話則會發出 BEEP 聲,並啟動 LED 燈閃爍機制
- 信標模式,則是每2ms會執行一次 tm_apfunc()。在第一次執行時,會檢查設定,第二次發出信標,第三次進入省電模式。所以在ESP8266都初始完成後,大概又花了6ms發出信標後,又進入省電模式。下次醒來後,就會從 user_init() 又開始執行。
細部程式
程式的說明,我們就都內嵌在程式碼內了,這樣解說方便些。
user_init() – 主程式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
void user_init(void) { int pin_opmode, opmode; /* 先把 RTC 內的 memory 讀出來,由於進省電模式後,主CPU的RAM資料會遺失 。我們把一些狀態,記錄在 RTC 內 */ system_rtc_mem_read(64, &rtc, sizeof(rtc)); /* 如果不含 magic number, 0xbeefbeef。就是第一次開機,先初始化 並設定在稍後要發出 BEEP */ if(rtc.magic != 0xbeefbeef) { memset(&rtc,0, sizeof(rtc)); rtc.shouldBeep = 1; rtc.magic = 0xbeefbeef; } /* 取得目前的 WiFi Mode, 不是AP Mode,就切成 AP Mode。 */ opmode = wifi_get_opmode(); if(opmode != SOFTAP_MODE) wifi_set_opmode(SOFTAP_MODE); /* 設定 UART 成 115200 */ uart_div_modify(0, UART_CLK_FREQ / 115200); /* 讀取 GPIO5,看看目前設定的模試是搜尋還是信標模式 */ pin_opmode = mode_detect(); /* 把蜂鳴器的 PIN ,設成 OUTPUT Mode */ GPIO_AS_OUTPUT(1<<BUZZER_PIN); /* 如果目前是運作在信標模式 */ if(pin_opmode == SOFTAP_MODE) { wifi_get_macaddr(0x01, macaddr); /* 取得 WiFi MAC,要用做信標的ID */ if(rtc.shouldBeep == 1) { /* 第一次開機 */ /* 將使用者設定區的設定值讀出來 */ system_param_load(ESP_PARAM_START_SEC, 0, &user_cfg, sizeof(user_cfg)); /* 如果不含 MAGIC,代表是第一次使用這塊模組,將參數初始化 */ if(user_cfg.magic != WIFI995_MAGIC) { clear_user_cfg(); } /* 套用使用者設定的省電模式長度,預設是7.5秒,最短是2秒 */ rtc.wake_interval = user_cfg.wake_interval; if(rtc.wake_interval <=0 ) rtc.wake_interval = 7500; else if(rtc.wake_interval < 2000) rtc.wake_interval = 2000; /* 第一次開機,發出500ms beep */ ap_startup_beep(); rtc.shouldBeep = 0; /* 將系統狀態存到 RTC 記憶體 */ system_rtc_mem_write(64, &rtc,sizeof(rtc)); } os_timer_disarm(&tm); os_timer_setfn(&tm, (os_timer_func_t *)tm_apfunc, NULL); os_timer_arm(&tm, 2, 1); wifi_softap_dhcps_stop(); system_deep_sleep_set_option(2); } else { ssc_attach(SSC_BR_115200); ssc_register(cmdtbl, CMDTBL_SIZE, NULL); wifi_set_opmode_current(STATION_MODE); os_timer_disarm(&tm); os_timer_setfn(&tm, (os_timer_func_t *)tm_stafunc, cmd_unknown); os_timer_arm(&tm, 300, 1); } os_printf("SDK version:%s\n", system_get_sdk_version()); } |
tm_apfunc() – 信標定時器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
void tm_apfunc(void *arg) { static int i=0; /* 雖是 static,進省電模式後,下次醒來還是會變0 */ struct softap_config cfg; if(i == 0) { /* 第一次執行 timer callback */ /* 判斷此模組 WiFi995 信標設定是否尚未設置,是的話就套用設定 */ wifi_softap_get_config(&cfg); /* WiFi995 的信標,其 SSID 是以 'HelloKitty10_' 為開頭 其實這個設置並沒用上,因為最後信標的內容是我們自己填的, 這裡只是將之當做 MAGIC Number 用 */ if(strstr(cfg.ssid, "HelloKitty10_") != (char *)cfg.ssid) { wifi_set_opmode(SOFTAP_MODE); wifi_set_phy_mode(PHY_MODE_11B); strcpy(cfg.ssid, "HelloKitty10_"); cfg.channel =0; /* 用 Channel 1,當做發射頻率 */ cfg.authmode=AUTH_OPEN; /* 不加密 */ cfg.beacon_interval=100*2; /* WiFi Beacon Interval, 好像也不重要 */ wifi_softap_set_config(&cfg); /* 將之儲存 */ } } else if(i == 1) { /* 第二次執行 timer callback */ /* 發送信標。沒有在第一次就發送,是因為要設定其頻率、模試,可能會需要一些時間 。等2ms後,會比較穩定 */ send_mybcn(0); /* 把 UART FIFO 清除,不然進省電模式前可能要先等其輸出完,會浪費時間 */ SET_PERI_REG_MASK(UART_CONF0(0), UART_TXFIFO_RST);//RESET FIFO CLEAR_PERI_REG_MASK(UART_CONF0(0), UART_TXFIFO_RST); } else if(i == 2) { /* 第三次執行 timer callback,進省電模式。後面是多久後要醒來, 睡眠的時間是以us為單位 */ system_deep_sleep_instant(1000*rtc.wake_interval); } /* 每睡眠/醒來五次,做一次電壓檢查 */ if((rtc.wake_cnt % 5) == 0) voltage_check(1, 1); i++; } |
tm_stafunc() – 搜尋定時器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
void ICACHE_FLASH_ATTR tm_stafunc(void *arg) { static int i=0; struct softap_config cfg; int t; if(i == 10) { /* i==10 時,約是開機3秒後,才開始動作。忘記是為什麼了,可能是要等系統穩定 */ printf("System parameter loaded.\n\r"); conf_loaded=1; /* 將使用者設定區的值取出 */ system_param_load(ESP_PARAM_START_SEC, 0, &user_cfg, sizeof(user_cfg)); sta_startup_beep(); /* 發出搜尋模式啟動的蜂鳴器聲音 */ /* 若是沒找到MAGIC就是第一次使用,進行初始化 */ if(user_cfg.magic != WIFI995_MAGIC) { clear_user_cfg(); } /* 將WiFi模式設為 11B, Channel1,並設定為 Monitor Mode。有收到任何 WiFi 封包,都會呼叫 promisc_cb() 來處理 */ wifi_set_phy_mode(PHY_MODE_11B); wifi_set_channel(1); wifi_promiscuous_enable(0); wifi_set_promiscuous_rx_cb(promisc_cb); wifi_promiscuous_enable(1); } /* 發現信標時的閃燈控制。這邊的 BUZZER_PIN 可能要做適當修改,不然是跟蜂鳴器用同一 支 GPIO 的。 */ if(light) light--; if(light == 0) GPIO_OUTPUT_SET(BUZZER_PIN, 0); else if(light & 1) GPIO_OUTPUT_SET(BUZZER_PIN, 1); else GPIO_OUTPUT_SET(BUZZER_PIN, 0); i++; /* 每300ms都檢查一次電壓 */ voltage_check(i, 5); } |
send_mybcn() – 發送信標
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
void send_mybcn(uint8 status) { int len; /* 信標內容已預先定好,只需修改 SSID 部份 */ static char bcn[] = { /*00*/ 0x80, 0x00, 0x00, 0x00, //header, duration /*04*/ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, //dst /*10*/ 0x5e, 0xcf, 0x7f, 0x82, 0xc0, 0x89, //src /*16*/ 0x5e, 0xcf, 0x7f, 0x82, 0xc0, 0x89, //bssid /*22*/ 0x00, 0x00, //seq , frag /*24*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //timestamp /*32*/ 0x64, 0x00, //bcn interval /*34*/ 0x01, 0x00, //cap /*36*/ 0x00, 15, //SSID IE /*38*/ 0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,0x9,0x10,0x11,0x12,0x13,0x14,0x0,//ssid //0x01, 0x08, 0x8B, 0x96, 0x82, 0x84, 0x0C, 0x18, 0x30, 0x60, //SupRate, //0x03, 0x01, 0x0, //dsss //0x05, 0x04, 0x01, 0x02, 0x00, //TIM }; /* 信標識別名稱(利用SSID IE)是在第38個byte, 長度為15。後面6個字元是MAC Address 的後三個byte */ sprintf(bcn+38, "WiFi995_%02x%02x%02x", macaddr[3], macaddr[4],macaddr[5]); os_printf("%c%c%c\n", 'b', 'c', 'n'); /* 呼叫發送資料的 API. 後面的1是 seq number. 0 則會自動增加 。這邊並沒有速度可設置,我猜測會根據你的 frame type 來決定。 management frame 在 11B 是用 1Mbps 發送的。 */ wifi_send_pkt_freedom(bcn, sizeof(bcn), 1); } |
promisc_cb() – 判斷信標
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
void ICACHE_FLASH_ATTR promisc_cb(uint8 *buf, uint16 len) { int i; static int cnt=0; char *p; /* 比對固定位置,看是不是要尋找的信標代碼。 */ if(is_ssid_mon(buf+50)) { /* 印出WiFi995_後面的六個字,是信標的MAC後3個byte */ p = buf+58; os_printf("[Found] %c%c%c%c%c%c ", p[0], p[1], p[2], p[3], p[4], p[5]); /* 忘記為何128byte後面是信標 rssi 了,可能要參考 SDK */ if(len == 128) { struct sniffer_buf2 *pkt = (struct sniffer_buf2 *)buf; printf(" rssi=%d", pkt->rx_ctrl.rssi); } printf("\n"); /* 設定變數,會在 tm_stafunc() 裡閃燈 */ light=user_cfg.found_beep; /* 用 GPIO 模擬蜂鳴器,裡面會在排 timer 做 GPIO 切換 */ start_mybeep(500); } } |
voltage_check() – 檢查電壓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
void voltage_check(int sample_cnt, int avg_cnt) { int t; /* 取得目前電壓 ADC 值,稍候再做平均 */ rtc.adc+=system_adc_read(); /* 如果達到要顯示的 criteria */ if(sample_cnt % avg_cnt == 0) { /* 平均 adc 值 */ rtc.adc/=avg_cnt; /* adc 轉電壓, 用查表法取得 */ t=adc_to_vol(rtc.adc); /* Low Voltage Report */ if(rtc.lv_report) { printf("[ADC] %d(%d.%d)\n", rtc.adc, t/10, t % 10 ); rtc.lv_report --; } //220ohm + 100ohm /* 由於ADC不是很精確,電壓連續低於 2.1v 五次後才亮警示燈 */ if(adc_to_vol(rtc.adc) <= 21) rtc.lv_cnt++; else rtc.lv_cnt=0; rtc.adc=0; } /* 連續低電壓超過5次,就點亮警示燈 */ if(rtc.lv_cnt>=5) GPIO_OUTPUT_SET(LPWR_PIN, 1); /* 信標模式也要做低電壓警示,所以要把資料寫到 RTC Memory */ system_rtc_mem_write(64, &rtc,sizeof(rtc)); } |
sta_startup_beep() – 搜尋模式啟動聲音
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void ICACHE_FLASH_ATTR sta_startup_beep() { /* 此段程式碼,直覺上是針對有源蜂鳴器的,無源蜂鳴器並無法發出聲音才對 無源蜂鳴器的行為會更複雜一點。若以有源蜂鳴器,此段程式碼,會以長-短-短 的聲音發出開機程式的警示 */ GPIO_OUTPUT_SET(BUZZER_PIN, 1); mdelay(500); GPIO_OUTPUT_SET(BUZZER_PIN, 0); mdelay(100); GPIO_OUTPUT_SET(BUZZER_PIN, 1); mdelay(100); GPIO_OUTPUT_SET(BUZZER_PIN, 0); mdelay(100); GPIO_OUTPUT_SET(BUZZER_PIN, 1); mdelay(100); GPIO_OUTPUT_SET(BUZZER_PIN, 0); } |
start_mybeep(), mybeep() – 發現搜尋信標警示音
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
void mybeep(int beep) { /* 1ms 的 HIGH 與 LOW 方波,以產生聲音 */ if(mybeep_time & 1) GPIO_OUTPUT_SET(BUZZER_PIN, 1); else GPIO_OUTPUT_SET(BUZZER_PIN, 0); mybeep_time--; /* beep count 到達微,就解除 timer */ if(mybeep_time <= 0) { os_timer_disarm(&tm2); } } void start_mybeep(int beep) { mybeep_time = beep; os_timer_disarm(&tm2); os_timer_setfn(&tm2, (os_timer_func_t *)mybeep, NULL); /* 安排以 1ms 為週期的 timer, 來拉 GPIO, 以產生示音 os_timer_arm(&tm2, 1, 1); } |
is_ssid_mon() – 比對信標識別碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int ICACHE_FLASH_ATTR is_ssid_mon(char *ssid) { int i; /* 前面7個字要符合 "WiFi995" */ if(memcmp(ssid, ssid_magic, 7) != 0) return 0; /* 若設定為任意信標就回報 */ if(user_cfg.monAll) return 1; /* 比對所有要監測的信標識別碼 */ for(i=0;i<MAXMON;i++) if(user_cfg.monList[i].en) if(memcmp(ssid+8, user_cfg.monList[i].m, sizeof(user_cfg.monList[i].m)) == 0) return 1; return 0; } |
其它程式
剩下沒解說到的程式碼,大部份就是比較直覺的。像是 parse/save user config data 或是透過API dump system memory 。還蠻多的,就不再贅述。
值得提的一點是,由於 ESP8266 使用者可用的 RAM 大約是40K 左右,如果把全部的程式碼放到 RAM,有時就會產生不足的現象。並非所有的程式都需要一次全部載入到 RAM 裡面,透過 ICACHE_FLASH_ATTR 修飾語句,可以讓function在執行時才載入到記憶體中。這特別適合那些 CLI 的測試功能。