配置腳本:
【startup.sh】
#!/bin/sh
insmod usb_wwan.ko
insmod qcserial.ko
./startup
【startup.c】
/**
* @file startup.c
* EC200M 4G模塊開機初始化程序
*/
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/uio.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#define RESTART (-2) ///< 模塊正在重啟
/** AT命令回應 */
struct response
{
char buffer[1024]; ///< 存放多個字符串的緩衝區
char *lines[20]; ///< 收到的回應的每一行的內容(不含換行符)
int line_count; ///< 收到的回應的行數
};
int verbose = 0; ///< 是否在控制台列印詳細日誌
/**
* 解析命令行參數。
*
* @param argc 參數個數。
* @param argv 參數列表。
* @retval 0 解析成功。
* @retval -1 解析失敗,命令行參數有誤。
*/
int parse_arguments(int argc, char *argv[])
{
int i;
for (i = 1; i < argc; i++)
{
if (strcmp(argv[i], "-v") == 0)
verbose = 1;
else
{
printf("unknown argument: %s\n", argv[i]);
return -1;
}
}
return 0;
}
/**
* 配置串口參數。
*
* @param fd 串口的文件描述符。
* @param baudrate 串口波特率。
* @retval 0 配置成功。
* @retval -1 配置失敗。
*/
int configure_uart(int fd, int baudrate)
{
int ret;
struct termios attr;
ret = tcgetattr(fd, &attr);
if (ret == -1)
{
perror("tcgetattr() failed");
return -1;
}
cfsetspeed(&attr, baudrate);
attr.c_cflag = (attr.c_cflag & ~CSIZE) | CS8; // 8位數據
attr.c_cflag &= ~PARENB; // 無奇偶校驗
attr.c_cflag &= ~CSTOPB; // 1位停止位
attr.c_cflag &= ~CRTSCTS; // 禁用硬體流控
attr.c_cflag |= CREAD | CLOCAL; // 啟用接收
attr.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 原始輸入
attr.c_oflag &= ~OPOST; // 原始輸出
// 立即返回讀取結果
attr.c_cc[VMIN] = 0;
attr.c_cc[VTIME] = 10; // 1秒超時
ret = tcsetattr(fd, TCSANOW, &attr);
if (ret == -1)
{
perror("tcsetattr() failed");
return -1;
}
return 0;
}
/**
* 發送AT命令並接收命令回應。
*
* @param fd 串口的文件描述符。
* @param cmd 要發送的AT命令字符串,末尾不用加換行符。
* @param resp 接收AT命令回應用的結構體。
* @param timeout 超時時間。填0表示採用默認的超時時間。
* @retval 1 命令回應接收成功,回應內容表示AT命令執行成功(OK)。
* @retval 0 命令回應接收成功,回應內容表示AT命令執行失敗(ERROR)。
* @retval -1 命令回應接收超時。
*
@attention 串口回顯字符和非當前命令的回應(異步通知消息)會被忽略,不會保存到resp結構體當中。
*/
int send_command(int fd, const char *cmd, struct response *resp, int timeout)
{
char name[50] = "";
char *p;
int curr, i, namelen, ret;
struct iovec iov[2];
time_t start, now;
memset(resp, 0, sizeof(struct response));
if (timeout == 0) // 0表示採用默認的超時時間
timeout = 1000;
// 發送命令
iov[0].iov_base = (void *)cmd;
iov[0].iov_len = strlen(cmd);
iov[1].iov_base = "\r\n";
iov[1].iov_len = 2;
ret = writev(fd, iov, 2);
if (ret == -1)
{
printf("%s: writev() failed\n", __func__);
return -1;
}
// 提取命令名稱
if (cmd[2] == '+')
{
for (i = 3; cmd[i] != '\0' && cmd[i] != '=' && cmd[i] != '?'; i++)
{
if (i - 3 == sizeof(name) - 1)
break;
name[i - 3] = cmd[i];
}
name[i - 3] = '\0';
}
namelen = strlen(name);
// 接收回應
time(&start);
i = 0;
do
{
curr = sizeof(resp->buffer) - i - 1;
ret = read(fd, resp->buffer + i, curr);
if (ret == -1)
{
printf("%s: read() failed\n", __func__);
return -1;
}
i += ret;
resp->buffer[i] = '\0';
if (strstr(resp->buffer, "OK") != NULL)
ret = 1;
else if (strstr(resp->buffer, "ERROR") != NULL)
ret = 0;
else
ret = -1; // 回應未接收完畢
time(&now);
} while (ret == -1 && now - start < timeout / 1000);
// 拆分成字符串數組
for (p = strtok(resp->buffer, "\r\n"); p != NULL; p = strtok(NULL, "\r\n"))
{
if (resp->line_count == 0 && strcmp(p, cmd) == 0)
continue; // 忽略回顯字符
if (p[0] == '+' && (strncmp(p + 1, name, namelen) != 0 || p[1 + namelen] != ':'))
{
printf("%s\n", p);
continue; // 忽略非當前命令的回應
}
resp->lines[resp->line_count] = p;
resp->line_count++;
if (resp->line_count == sizeof(resp->lines) / sizeof(resp->lines[0]))
break;
}
// 列印回應內容
if (verbose)
{
printf("[Command] %s\n", cmd);
for (i = 0; i < resp->line_count; i++)
printf("%d: %s\n", i, resp->lines[i]);
}
else
{
if (ret == 0)
printf("[Command] %s (error)\n", cmd);
else if (ret == -1)
printf("[Command] %s (timeout)\n", cmd);
}
return ret;
}
/**
* 測試指定串口是否能正確響應AT命令。
*
* @param fd 串口的文件描述符。
* @retval 1 串口能正確響應AT命令。
* @retval 0 串口不能正確響應AT命令。
*/
int is_device_ready(int fd)
{
int ret;
struct response resp;
ret = send_command(fd, "AT", &resp, 0);
if (ret == 0) // 如果返回了ERROR
ret = send_command(fd, "AT", &resp, 0); // 重試
return ret == 1; // 返回是否成功
}
/**
* 重啟設備。
*
* @param fd 串口的文件描述符。
* @retval -1 設備未響應重啟命令。
* @retval RESTART 設備已響應重啟命令。
*
@attention 設備重啟時,/dev/ttyUSB1設備文件會消失,導致文件描述符fd失效。\n
* 調用本函數成功後,請立即關閉文件描述符fd。
*/
int restart_device(int fd)
{
int ret;
struct response resp;
ret = send_command(fd, "AT+CFUN=1,1", &resp, 0);
if (ret != 1)
{
printf("failed to restart the device\n");
return -1;
}
return RESTART;
}
/**
* 切換到RNDIS模式(usbnet 3號模式)。
*
* @param fd 串口的文件描述符。
* @retval 0 已處於RNDIS模式,無需切換。
* @retval -1 切換失敗。
* @retval RESTART 設備已響應切換命令並且正在重啟。
* @note 切換成功並重啟模塊後會出現usb0網絡接口。\n
* EC200M模塊的USB interface 0和1(Mobile RNDIS Network Adapter)用的是linux內核自帶的RNDIS驅動。\n
* 在usbnet 3號模式下,板子開機後不需要insmod任何驅動模塊,就能自動產生usb0網絡接口。
*
@attention 設備重啟時,/dev/ttyUSB1設備文件會消失,導致文件描述符fd失效,請立即關閉文件描述符fd。
*/
int enable_rndis_mode(int fd)
{
int mode, ret;
struct response resp;
ret = send_command(fd, "AT+QCFG=\"usbnet\"", &resp, 0);
if (ret != 1)
return -1;
ret = sscanf(resp.lines[0], "+QCFG: \"usbnet\",%d", &mode);
if (ret != 1)
{
printf("%s: sscanf() ret=%d\n", __func__, ret);
return -1;
}
printf("usbnet mode: %d\n", mode);
if (mode == 3)
return 0;
ret = send_command(fd, "AT+QCFG=\"usbnet\",3", &resp, 0);
if (ret != 1)
return -1;
return restart_device(fd);
}
/**
* 激活網絡。
*
* @param fd 串口的文件描述符。
* @param apn 接入點名稱(Access Point Name)。一般和SIM卡的運營商有關。
* @retval 0 激活網絡成功。
* @retval -1 激活網絡失敗。
* @note 激活網絡成功後,linux系統裡面就可以上網了,可以ping通百度。
*/
int activate_network(int fd, const char *apn)
{
char cmd[100];
char *p, *q;
int connect_type, id, ret;
struct response resp;
// 檢查場景是否已激活
ret = send_command(fd, "AT+QIACT?", &resp, 0);
if (ret != 1)
return -1; // 檢查失敗
if (strcmp(resp.lines[0], "OK") == 0)
{
// 只返回了OK,表示場景未激活, 需要激活場景
snprintf(cmd, sizeof(cmd), "AT+QICSGP=1,1,\"%s\","","",1", apn);
ret = send_command(fd, cmd, &resp, 0);
if (ret != 1)
return -1;
// 受網絡狀態影響,執行AT+QIACT後,等待返回結果OK或者ERROR的最大時間為150秒,在結果尚未返回之前,無法執行任何AT命令。
ret = send_command(fd, "AT+QIACT=1", &resp, 150);
if (ret != 1)
return -1; // 激活失敗
ret = send_command(fd, "AT+QIACT?", &resp, 0);
if (ret != 1)
return -1;
}
// 顯示PDP場景編號
ret = sscanf(resp.lines[0], "+QIACT:%d", &id);
if (ret != 1)
{
printf("%s: sscanf() ret=%d\n", __func__, ret);
return -1;
}
printf("PDP Context ID: %d\n", id);
// 顯示IP位址
p = strchr(resp.lines[0], '"');
if (p == NULL)
{
printf("IP address: unknown\n");
return -1;
}
p++;
q = strrchr(p, '"');
if (q != NULL)
*q = '\0';
printf("IP address: %s\n", p);
// 允許網絡數據包傳遞到linux內核
ret = send_command(fd, "AT+QNETDEVCTL?", &resp, 0);
if (ret != 1)
return -1;
ret = sscanf(resp.lines[0], "+QNETDEVCTL: %d\n", &connect_type);
if (ret != 1)
{
printf("%s: sscanf() ret=%d\n", __func__, ret);
return -1;
}
if (connect_type != 3)
{
ret = send_command(fd, "AT+QNETDEVCTL=3,1,1", &resp, 0);
if (ret != 1)
return -1;
ret = send_command(fd, "AT+QNETDEVCTL?", &resp, 0);
if (ret != 1)
return -1;
ret = sscanf(resp.lines[0], "+QNETDEVCTL: %d\n", &connect_type);
if (ret != 1)
{
printf("%s: sscanf() ret=%d\n", __func__, ret);
return -1;
}
if (connect_type != 3)
{
printf("%s: connect_type=%d\n", __func__, connect_type);
return -1;
}
}
return 0;
}
/**
* 主函數。
*
* @param argc 參數個數。
* @param argv 參數列表。
* @retval 0 程序運行成功。
* @retval -1 程序運行出錯。
*/
int main(int argc, char *argv[])
{
int fd = -1;
int ret;
ret = parse_arguments(argc, argv);
if (ret == -1)
return -1;
restart:
if (fd != -1)
{
close(fd);
fd = -1;
do
{
sleep(10);
} while (access("/dev/ttyUSB1", 0) == -1);
printf("device restarted\n");
}
fd = open("/dev/ttyUSB1", O_RDWR);
if (fd == -1)
{
perror("open() failed");
return -1;
}
ret = configure_uart(fd, 115200);
if (ret == -1)
goto end;
if (!is_device_ready(fd))
{
printf("device is not ready\n");
ret = -1;
goto end;
}
ret = enable_rndis_mode(fd);
if (ret == -1)
{
printf("failed to enable RNDIS mode\n");
goto end;
}
else if (ret == RESTART)
goto restart;
ret = activate_network(fd, "cmnet");
if (ret == -1)
{
printf("failed to activate network\n");
goto end;
}
ret = 0;
end:
close(fd);
return ret;
}