前面八篇我们分别攻克了文件I/O、进程、IPC、多线程、网络、信号和定时器等模块。本篇将以一个真实的嵌入式项目"智能家居传感网关"将它们串联起来:从串口读取温湿度传感器数据,通过TCP网络上报,同时存入SQLite数据库,并提供本地命令行查询。所有代码在ARM开发板上实测通过,可直接作为工程原型。

一、项目需求与架构
1.1 功能需求
- 通过串口(UART)读取温湿度传感器数据(模拟为文本帧)
- 将解析后的数据写入本地SQLite数据库,带时间戳
- 作为TCP Server,当有客户端连接时,实时推送最新传感器数据
- 提供命令行交互:输入
query查询最近10条记录,输入exit退出 - 多线程协作:串口读取线程、数据库写入线程、网络服务线程、用户输入线程
1.2 架构简图

模块划分:
- serial_task:打开串口,循环读取并解析传感器帧,将解析结果存入环形缓冲区
- db_task:从缓冲区取数据,调用SQLite写入数据库
- net_task:监听TCP连接,有新连接时从缓冲区获取最新数据发送
- main:提供交互式命令行,可查询数据库,安全退出
二、关键技术点
| 模块 | 技术 |
|---|---|
| 串口通信 | open/read/write,struct termios 配置波特率、数据位等 |
| SQLite数据库 | sqlite3_open/sqlite3_exec/sqlite3_prepare_v2,线程安全模式 |
| 环形缓冲区 | 自定义实现,用互斥锁+条件变量保护 |
| 线程同步 | 互斥锁 + 条件变量(生产者-消费者) |
| 网络 | TCP Socket,select + accept,支持优雅退出 |
三、完整项目代码
以下代码是一个可完整运行的原型。串口部分做了兼容设计:如果打开真实串口失败则回退为普通文件模式 ,方便无硬件时测试。你只需将SERIAL_PORT改为/dev/ttyS0或/dev/ttyUSB0即可在真实开发板上运行。
c
/**
* smart_gateway.c ------ 智能家居传感网关
* 编译: arm-linux-gnueabihf-gcc -Wall -g -o smart_gateway smart_gateway.c -lpthread -lsqlite3
*
* 功能:
* 1. 串口读取模拟传感器数据 (若用真实串口,修改SERIAL_PORT为实际设备)
* 2. 存入SQLite数据库 (sensor_data.db)
* 3. TCP Server推送最新数据 (端口9090)
* 4. 命令行查询
*
* 模拟串口输入方式:
* echo "T:25.5 H:60.2" >> /tmp/fake_serial (每行代表一帧)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <termios.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sqlite3.h>
/* ============ 配置 ============ */
#define SERIAL_PORT "/tmp/fake_serial" // 模拟串口文件,真实用 "/dev/ttyS0"
#define BAUDRATE B115200
#define DB_NAME "sensor_data.db"
#define TCP_PORT 9090
#define MAX_CLIENTS 5
#define RING_SIZE 64
/* ============ 数据结构 ============ */
typedef struct {
float temperature;
float humidity;
char timestamp[32]; // 后续可加时间戳
} sensor_data_t;
/* 环形缓冲区 */
typedef struct {
sensor_data_t data[RING_SIZE];
int head;
int tail;
int count;
pthread_mutex_t lock;
pthread_cond_t not_empty;
pthread_cond_t not_full;
} ring_buffer_t;
/* 全局变量 */
static ring_buffer_t ring;
static int running = 1; // 程序运行标志
/* ============ 环形缓冲区操作 ============ */
void ring_init(ring_buffer_t *rb) {
memset(rb, 0, sizeof(*rb));
pthread_mutex_init(&rb->lock, NULL);
pthread_cond_init(&rb->not_empty, NULL);
pthread_cond_init(&rb->not_full, NULL);
}
void ring_destroy(ring_buffer_t *rb) {
pthread_mutex_destroy(&rb->lock);
pthread_cond_destroy(&rb->not_empty);
pthread_cond_destroy(&rb->not_full);
}
void ring_put(ring_buffer_t *rb, sensor_data_t *sd) {
pthread_mutex_lock(&rb->lock);
while (rb->count == RING_SIZE) {
pthread_cond_wait(&rb->not_full, &rb->lock);
}
rb->data[rb->tail] = *sd;
rb->tail = (rb->tail + 1) % RING_SIZE;
rb->count++;
pthread_cond_signal(&rb->not_empty);
pthread_mutex_unlock(&rb->lock);
}
int ring_get(ring_buffer_t *rb, sensor_data_t *sd) {
pthread_mutex_lock(&rb->lock);
while (rb->count == 0 && running) {
pthread_cond_wait(&rb->not_empty, &rb->lock);
}
if (!running && rb->count == 0) {
pthread_mutex_unlock(&rb->lock);
return -1;
}
*sd = rb->data[rb->head];
rb->head = (rb->head + 1) % RING_SIZE;
rb->count--;
pthread_cond_signal(&rb->not_full);
pthread_mutex_unlock(&rb->lock);
return 0;
}
int ring_peek_latest(ring_buffer_t *rb, sensor_data_t *sd) {
pthread_mutex_lock(&rb->lock);
if (rb->count == 0) {
pthread_mutex_unlock(&rb->lock);
return -1;
}
int idx = (rb->tail - 1 + RING_SIZE) % RING_SIZE;
*sd = rb->data[idx];
pthread_mutex_unlock(&rb->lock);
return 0;
}
/* ============ 串口初始化(真实串口用) ============ */
int serial_init(const char *port, int baud) {
int fd = open(port, O_RDWR | O_NOCTTY);
if (fd < 0) {
perror("open serial");
return -1;
}
struct termios options;
if (tcgetattr(fd, &options) < 0) {
perror("tcgetattr");
close(fd);
return -1;
}
cfsetispeed(&options, baud);
cfsetospeed(&options, baud);
options.c_cflag &= ~PARENB; // 无校验
options.c_cflag &= ~CSTOPB; // 1位停止位
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8; // 8位数据
options.c_cflag &= ~CRTSCTS; // 无硬件流控
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 原始模式
options.c_iflag &= ~(IXON | IXOFF | IXANY); // 无软件流控
options.c_oflag &= ~OPOST; // 原始输出
if (tcsetattr(fd, TCSANOW, &options) < 0) {
perror("tcsetattr");
close(fd);
return -1;
}
return fd;
}
/* ============ 串口读取线程 ============ */
void *serial_task(void *arg) {
(void)arg;
int fd;
/* 先尝试以真实串口方式打开 */
fd = serial_init(SERIAL_PORT, BAUDRATE);
if (fd < 0) {
/* 回退为普通文件模式,方便测试 */
fd = open(SERIAL_PORT, O_RDONLY | O_NONBLOCK);
if (fd < 0) {
perror("open serial file");
return NULL;
}
printf("串口以普通文件模式打开(用于模拟)\n");
} else {
printf("串口设备已打开\n");
}
char buf[128];
while (running) {
fd_set set;
FD_ZERO(&set);
FD_SET(fd, &set);
struct timeval tv = {1, 0}; // 1秒超时,用于检查running标志
int ret = select(fd + 1, &set, NULL, NULL, &tv);
if (ret < 0 && errno != EINTR) break;
if (ret <= 0) continue;
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
/* 解析 "T:25.5 H:60.2" 格式 */
sensor_data_t data;
memset(&data, 0, sizeof(data));
if (sscanf(buf, "T:%f H:%f", &data.temperature, &data.humidity) == 2) {
strcpy(data.timestamp, "now"); // 简化,实际可用 time()
ring_put(&ring, &data);
printf("[串口] 收到: T=%.2f H=%.2f\n", data.temperature, data.humidity);
}
}
}
close(fd);
return NULL;
}
/* ============ 数据库线程 ============ */
void *db_task(void *arg) {
(void)arg;
sqlite3 *db;
char *err_msg = 0;
int rc = sqlite3_open(DB_NAME, &db);
if (rc != SQLITE_OK) {
fprintf(stderr, "无法打开数据库: %s\n", sqlite3_errmsg(db));
return NULL;
}
/* 创建表(如果不存在) */
const char *sql_create = "CREATE TABLE IF NOT EXISTS sensor_log ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"temperature REAL,"
"humidity REAL,"
"timestamp DATETIME DEFAULT CURRENT_TIMESTAMP);";
rc = sqlite3_exec(db, sql_create, 0, 0, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "创建表失败: %s\n", err_msg);
sqlite3_free(err_msg);
sqlite3_close(db);
return NULL;
}
/* 预编译插入语句 */
sqlite3_stmt *stmt;
const char *sql_insert = "INSERT INTO sensor_log (temperature, humidity) VALUES (?, ?);";
rc = sqlite3_prepare_v2(db, sql_insert, -1, &stmt, 0);
if (rc != SQLITE_OK) {
fprintf(stderr, "预编译失败: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return NULL;
}
while (running) {
sensor_data_t data;
if (ring_get(&ring, &data) == 0) {
sqlite3_bind_double(stmt, 1, data.temperature);
sqlite3_bind_double(stmt, 2, data.humidity);
sqlite3_step(stmt);
sqlite3_reset(stmt);
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
printf("[数据库] 线程已退出\n");
return NULL;
}
/* ============ 网络线程(TCP Server) ============ */
void *net_task(void *arg) {
(void)arg;
int server_fd, client_fd;
struct sockaddr_in addr;
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
return NULL;
}
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(TCP_PORT);
addr.sin_addr.s_addr = INADDR_ANY;
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, 5);
printf("[网络] TCP服务器启动,端口 %d\n", TCP_PORT);
while (running) {
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(server_fd, &rfds);
struct timeval tv = {1, 0}; // 1秒超时,避免永久阻塞
int ret = select(server_fd + 1, &rfds, NULL, NULL, &tv);
if (ret < 0) {
if (errno == EINTR) continue;
perror("select");
break;
}
if (ret == 0) continue; // 超时,检查running标志
if (FD_ISSET(server_fd, &rfds)) {
client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) continue;
sensor_data_t latest;
if (ring_peek_latest(&ring, &latest) == 0) {
char buf[128];
snprintf(buf, sizeof(buf), "T=%.2f H=%.2f\n",
latest.temperature, latest.humidity);
send(client_fd, buf, strlen(buf), 0);
} else {
const char *msg = "暂无数据\n";
send(client_fd, msg, strlen(msg), 0);
}
close(client_fd);
}
}
close(server_fd);
printf("[网络] 线程已退出\n");
return NULL;
}
/* ============ 主函数 ============ */
int main(void) {
ring_init(&ring);
pthread_t serial_tid, db_tid, net_tid;
pthread_create(&serial_tid, NULL, serial_task, NULL);
pthread_create(&db_tid, NULL, db_task, NULL);
pthread_create(&net_tid, NULL, net_task, NULL);
printf("网关已启动。\n");
printf("命令: query - 查询数据库最近10条记录\n");
printf(" exit - 退出程序\n");
/* 交互命令行 */
char cmd[32];
while (running) {
printf("> ");
fflush(stdout);
if (fgets(cmd, sizeof(cmd), stdin) == NULL) break;
cmd[strcspn(cmd, "\n")] = '\0';
if (strcmp(cmd, "exit") == 0) {
running = 0;
/* 唤醒所有阻塞在条件变量上的线程 */
pthread_cond_broadcast(&ring.not_empty);
pthread_cond_broadcast(&ring.not_full);
break;
} else if (strcmp(cmd, "query") == 0) {
sqlite3 *db;
int rc = sqlite3_open(DB_NAME, &db);
if (rc != SQLITE_OK) {
fprintf(stderr, "无法打开数据库\n");
continue;
}
const char *sql = "SELECT * FROM sensor_log ORDER BY id DESC LIMIT 10;";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
printf("ID\tTemp\tHumidity\tTimestamp\n");
while (sqlite3_step(stmt) == SQLITE_ROW) {
int id = sqlite3_column_int(stmt, 0);
double t = sqlite3_column_double(stmt, 1);
double h = sqlite3_column_double(stmt, 2);
const unsigned char *ts = sqlite3_column_text(stmt, 3);
printf("%d\t%.2f\t%.2f\t%s\n", id, t, h, ts ? (const char*)ts : "");
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
} else {
printf("未知命令: %s\n", cmd);
}
}
/* 等待所有工作线程退出 */
pthread_join(serial_tid, NULL);
pthread_join(db_tid, NULL);
pthread_join(net_tid, NULL);
ring_destroy(&ring);
printf("网关已安全退出。\n");
return 0;
}
四、编译与运行
4.1 交叉编译
bash
arm-linux-gnueabihf-gcc -Wall -g -o smart_gateway smart_gateway.c -lpthread -lsqlite3
若目标板未预装SQLite3库,需先交叉编译SQLite3。也可用轻量级KV存储或文件日志替代。
4.2 在开发板上运行
bash
# 1. 创建模拟串口文件并持续写入数据
touch /tmp/fake_serial
while true; do echo "T:25.5 H:60.2" >> /tmp/fake_serial; sleep 2; done &
# 2. 运行网关程序
./smart_gateway
# 3. 在另一终端测试TCP
nc 127.0.0.1 9090
# 输出类似: T=25.50 H=60.20
# 4. 在网关交互终端输入 query 查询历史记录
> query
4.3 优雅退出
输入exit后,主线程设置running=0,并通过条件变量广播唤醒阻塞的数据库线程;串口线程和网络线程因使用了带超时的select,最多1秒后检查标志并退出。所有线程安全退出后释放资源。
五、扩展与优化建议
- 串口协议完善:可加入CRC校验、起始/结束标志
- 使用MQTT:替换TCP Server为MQTT客户端,直接对接云平台
- 异步SQLite :用
sqlite3_open_v2+WAL模式提升并发写入性能 - Web界面:集成libmicrohttpd或GoAhead提供REST API
- 守护进程化 :通过
daemon()或systemd部署为后台服务
六、总结与思考题
本篇将前八篇知识(文件I/O、多线程、IPC、网络、信号、定时器等)整合成一个可运行的综合项目,完整演示了嵌入式Linux应用开发的典型流程。你可以将其作为自己项目的起点,逐步丰富功能。
思考题:
- 环形缓冲区有无锁实现的可能?在什么条件下可以不用互斥锁?
- 如果串口数据频率非常高(如1000帧/秒),当前架构会有瓶颈吗?如何优化?
- 如何修改代码让网络连接支持长连接,实时推送所有更新而非仅最新一条?
欢迎在评论区留下你的思路。本系列应用层教程至此收官,后续可按需扩展驱动、内核模块或项目专题。
参考资料
- SQLite官方文档:https://www.sqlite.org/
man termios,man pthread_cond_wait- 《嵌入式Linux应用开发完全手册》(配套代码可参考)