Linux物联网学习实践记录
前言
开发背景
从MCU物联网转向Linux物联网发展,入手了正点原子的RK3588开发板,但是和单片机时期不同的是实战例子很少,所以就记录一下自己手搓的过程。本文主要阐述从0开始搭建环境,最终实现通过AT指令的方式向指定IP发送数据。
一、Linux基本环境配置
1.Windows端环境配置
- 用VMware创建一个虚拟机,再安装Ubuntu20.04.具体创建过程不再赘述有很多例子,按照步骤一步一步来不会出错。VMware安装教程
- 安装传输软件,MobaXterm(显示开发板的串口打印,也可以通过SSH和虚拟机进行文件传输);FileZilla(windows 和 Ubuntu 相互传输文件,我习惯用这个,目录结构看的很清晰)
2.Ubuntu端环境配置
- 搭建FTP服务器
sudo apt-get update
sudo apt-get install vsftpd
sudo vi /etc/vsftpd.conf
安装完毕后,打开配置文件,去掉 line31 write_enable=YES 前的#号注释
- 搭建SSH服务器
sudo apt-get install openssh-server
- 安装VScode
打开Ubuntu自带的火狐浏览器,去VScode下载Linux系统版本的安装包,下载完成后用命令
sudo dpkg -i code_1.45.1-1589445302_amd64.deb
4.安装交叉编译器
就是一个翻译官,将你在Ubuntu环境下写的代码转为开发板环境下可执行的文件。所以需要先知道开发板是什么环境。
#查看系统版本
uname -a
Linux ATK-DLRK3588 5.10.160 #1 SMP Wed Oct 16 10:37:57 CST 2024 aarch64 GNU/Linux
然后在Ubuntu装对应的编译器,其他的gcc等可自行搜索安装
二、编写.c代码
在Linux中写代码和在mcu中有些不同,Linux是针对文件的操作,所以我们想要进行串口通信,需要先查明这个设备是挂载在哪里,然后对这个串口进行配置,然后再向这个串口写入字符串。
代码如下:
int fd;
struct termios options;
const char EM05_default_path[] = "/dev/ttyUSB2"; // 默认的EM05模块挂载路径
EM05_State_e EM05_FsmLoopState = EM05_STATE_DEFAULT;
// 初始化串口
int init_serial(SerialConfig *config, int *fd)
{
// 打开串口设备
*fd = open(config->device, O_RDWR | O_NOCTTY | O_SYNC);
if (*fd < 0)
{
perror("open_port: 打开串口设备失败");
return -1;
}
// 清空串口缓冲区
tcflush(*fd, TCIOFLUSH);
// 获取当前串口配置
tcgetattr(*fd, &options);
// 设置输入输出波特率
cfsetispeed(&options, config->baudrate);
cfsetospeed(&options, config->baudrate);
// 接收使能
options.c_cflag |= CREAD; // 接收使能
// 设置数据位
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
// 无校验位
options.c_cflag &= ~PARENB;
options.c_iflag &= ~INPCK;
// 1个停止位
options.c_cflag &= ~CSTOPB;
// MIN,TIME 设置为0
options.c_cc[VTIME] = 0;
options.c_cc[VMIN] = 0;
// 设置串口属性
if (tcsetattr(*fd, TCSANOW, &options) != 0)
{
perror("tcsetattr: 设置串口属性失败");
close(*fd);
return -1;
}
else
{
printf("串口初始化成功\r\n");
}
return 0;
}
// 发送AT命令并读取响应
int send_at_command(int fd, const char *cmd, char *response, size_t response_size)
{
char buffer[BUFFER_SIZE];
snprintf(buffer, sizeof(buffer), "%s\r\n", cmd);
if (write(fd, buffer, strlen(buffer)) < 0)
{
perror("写入串口失败");
return -1;
}
fd_set readfds;
struct timeval timeout;
int retval;
size_t total_read = 0;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
timeout.tv_sec = TIMEOUT_SECONDS;
timeout.tv_usec = 0;
while (1)
{
retval = select(fd + 1, &readfds, NULL, NULL, &timeout);
if (retval == -1)
{
perror("select调用失败");
return -1;
}
else if (retval == 0)
{
fprintf(stderr, "AT命令超时\n");
break; // 超时退出循环
}
int len = read(fd, response + total_read, response_size - total_read - 1);
if (len < 0)
{
perror("读取串口失败");
return -1;
}
total_read += len;
// 检查是否读取到了完整的响应(通常是以\r\n结尾)
if (strstr(response + total_read - 2, "\r\n") != NULL)
{
break; // 如果找到了\r\n,则认为响应完整,退出循环
}
// 如果缓冲区已满且没有找到\r\n,则可能需要更复杂的逻辑来处理(如扩展缓冲区或截断响应)
if (total_read >= response_size - 1)
{
fprintf(stderr, "响应缓冲区溢出\n");
break; // 缓冲区溢出,退出循环(这里应该根据实际情况决定如何处理)
}
// 重置select的超时时间,因为我们可能还没有读取到完整的响应
timeout.tv_sec = TIMEOUT_SECONDS; // 根据需要,这里可以设置为更短的时间以减少等待
timeout.tv_usec = 0;
// 清除readfds,因为select会修改它
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
}
// 确保响应以null字符结尾
response[total_read] = '\0';
// 去除响应末尾的回车换行符(如果存在)
char *newline_pos = strstr(response, "\r\n");
if (newline_pos != NULL)
{
*newline_pos = '\0';
}
printf("读取 %zu 字节: %s\n", total_read, response);
return 0;
}
// 关闭串口
void close_serial(int fd)
{
close(fd);
}
/**
* @description: 查询回复数据
* @param {char} *keyword 期望的答复
* @param {char} *source 实际返回的结果
* @return {*}
*/
int check_response(char *keyword, char *source)
{
char *pos = strstr(source, keyword);
if (pos == NULL)
return -1; // 关键字未找到
// 找到单词的开始位置
char *start = pos;
while (start > source && isalnum((unsigned char)*(start - 1)))
{
start--;
}
// 找到单词的结束位置
char *end = pos + strlen(keyword);
while (*end && isalnum((unsigned char)*end))
{
end++;
}
// 计算单词的长度(包括开始和结束之间的所有字符)
size_t length = end - start;
// 分配内存并复制单词
char *result = (char *)malloc(length + 1); // +1 for null terminator
if (result == NULL)
return -1; // 内存分配失败
strncpy(result, start, length);
result[length] = '\0'; // 添加空字符作为字符串结束符
return result;
}
// 主函数
int main()
{
SerialConfig config = {EM05_default_path, B115200};
int fd;
if (init_serial(&config, &fd) != 0)
{
fprintf(stderr, "串口初始化失败\n");
return EXIT_FAILURE;
}
// async_io_init();//调用该函数,初始化串口的异步I/O
char response[1024];
if (send_at_command(fd, "AT", response, sizeof(response)) != 0)
{
fprintf(stderr, "发送AT命令失败\n");
close_serial(fd);
return EXIT_FAILURE;
}
printf("AT命令响应: %s\n", response);
close_serial(fd);
return EXIT_SUCCESS;
}
三、编译和传输
#对.c文件进行编译
aarch64-linux-gnu-gcc -o emtest EM05_test.c
#传到开发板
scp emtest root@169.254.139.43:/
四、执行
在开发板文件目录下,执行,看到模块的OK回复指令,这样整个就是调通了。之后EM05的模块配置,关闭回显,获取APN,查看SIM卡,开启TCP连接等等,和MCU的流程一样。
总结
本文从0开始,通过串口与EM05模块进行通信,实现功能要求。