W5100Sio-M 模块通过HTTP协议接入豆包AI
本文介绍了基于W5100S以太网模块和STM32F103VCT6实现豆包AI接入的完整方案。主要内容包括:硬件连接采用W5100S的SPI接口与STM32连接;软件方面通过DNS解析、HTTP协议构建POST请求,实现了向豆包AI发送问题并解析响应的功能。方案包含NTP时间同步、看门狗保护等增强功能,通过串口交互方式完成用户与AI的对话测试。该方案提供了一种在嵌入式系统中集成AI服务的可行路径,适
目录
1 前言
HTTP(超文本传输协议,HyperText Transfer Protocol)是一种用于分布式、协作式、超媒体信息系统的应用层协议, 基于 TCP/IP 通信协议来传递数据,是万维网(WWW)的数据通信的基础。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法,通过 HTTP 或者 HTTPS 协议请求的资源由统一资源标识符(Uniform Resource Identifiers,URI)来标识。 以上是HTTP协议的简介,如想深入了解该协议,请参考mozilla网站上的介绍:HTTP 概述 - HTTP | MDN
W5100Sio-M 是炜世推出的高性能SPI转以太网模块,具有以下特点:
- 极简设计:集成MAC、PHY、16KB缓存及RJ45网口,通过4线SPI接口直连主控,3.3V供电,紧凑尺寸适配嵌入式场景 。
- 简单易用:用户无需再移植复杂的TCP/IP协议栈到MCU中,可直接基于应用层数据做开发。
- 资料丰富:提供丰富的MCU应用例程和硬件参考设计,可直接参考使用,大大缩短研发时间,硬件兼容W5500io-M模组,方便方案开发与迭代。
- 应用广泛:在工业控制、智能电网、充电桩、安防消防、新能源、储能等领域都有广泛应用。
产品链接:商品详情
2 项目环境
2.1 硬件环境
- W5100Sio-M
- STM32F103VCT6 EVB
- 网络连接线
- 杜邦线若干
- 交换机或路由器
2.2 软件环境
- 例程连接:https://www.w5100S.com
- 串口助手
- 豆包API
- 阿里云NTP服务器地址
- Keil5
3 硬件连接和方案
3.1 W5100S硬件连接
1. //W5100S_SCS ---> STM32_GPIOD7 /*W5100S的片选引脚*/
2. //W5100S_SCLK ---> STM32_GPIOB13 /*W5100S的时钟引脚*/
3. //W5100S_MISO ---> STM32_GPIOB14 /*W5100S的MISO引脚*/
4. //W5100S_MOSI ---> STM32_GPIOB15 /*W5100S的MOSI引脚*/
5. //W5100S_RESET---> STM32_GPIOD8 /*W5100S的RESET引脚*/
6. //W5100S_INT ---> STM32_GPIOD9 /*W5100S的INT引脚*/
3.2 方案图示
4 豆包AI参数获取
4.1 开通服务
进入账号登录-火山引擎,根据提示注册或登陆账号。
点击 Doubao-pro-4k 最右边的【开通服务】按钮,会弹出来一个弹窗。
在弹窗中勾选豆包的 6 个模型
(Doubao-pro-4k、Doubao-pro-8k、Doubao-pro-32k、Doubao-lite-4k、Doubao-lite-8k、Doubao-lite-32k),然后点击【立即开通】。
开通服务后每日单模型可享受50万免费tokens(1 个 token 大约为 4 个字符或 0.75 个单词)
4.2 创建 API Key
进入 API Key 管理,点击【创建 API Key】,填写名称后创建 API Key 备用。
4.3 创建接入点
豆包的模型不能直接使用,要先在平台内创建接入点了之后才能使用。
- 以 Doubao-pro-32k 为例:进入创建推理接入点,点击【创建推理接入点】。
- 点击【添加模型】按钮,会出现一个弹窗。
- 在模型广场中选择“Doubao-pro-32k”,然后右侧会出现模型版本。
- 模型版本一般只有一个,名称就是 6 个数字组成的日期(例如 240515),但也有可能会有带前缀的版本(例如 functioncall-240515、character-240528)。
- 模型版本要选择不带前缀的版本,即类似 240515 这样只有 6 个数字的版本。
- 选好模型版本后,点击页面右下角的【添加】。
- 名称建议就填写“接入模型”那里显示的文本,例如“Doubao-pro-32k-240515”(把斜线 / 改为短横线 -)。
- 点击页面右侧的【接入模型】按钮。
然后,你就会回到模型推理页面,此时表格中会看到你刚才创建的名为“Doubao-pro-32k-240515”的接入点,名称下方有一串以 ep- 开头的、格式为 ep-xxxxxxxxxx-xxxxx 的文本,这就是我们需要的接入点 ID,复制后备用。
获得以下参数就能正确调用豆包API了:
- 接口地址(OPENAI_BASE_URL)填写 https://ark.cn-beijing.volces.com/api/v3
- 密钥(OPENAI_API_KEY)填写你创建的 API Key
- 模型(OPENAI_MODEL)填写你创建的接入点 ID
5 例程修改
5.1 主函数文件修改
main.c文件修改如下:
主要功能包括
主要功能包括:
- 硬件初始化:配置 SPI 接口、USART 调试串口、定时器和看门狗
- 网络初始化:设置 W5100 网络参数(MAC、IP、网关等)
- NTP 时间同步:从阿里云 NTP 服务器获取网络时间
- 域名解析:通过 DNS 将 API 域名解析为 IP 地址
- 用户交互:通过串口接收用户问题并发送给 AI
#include "stm32f10x.h"
#include <stdio.h>
#include <string.h>
#include "wiz_platform.h"
#include "wizchip_conf.h"
#include "wiz_interface.h"
#include "do_dns.h"
#include "httpclient.h"
#include "stm32f10x_iwdg.h"
#include "ntp_client.h"
#define SOCKET_ID 0
#define ETHERNET_BUF_MAX_SIZE (1024 * 2)
/* 豆包API参数 */
#define DOUBAO_API_KEY "dc2972ae-fc48-4ecf-b113-9dff3668e70c"
#define MODEL_ID "ep-20241211221157-z2rfx"
#define API_DOMAIN "ark.cn-beijing.volces.com"
#define API_PATH "/api/v3/chat/completions"
#define API_PORT 80
/* NTP参数 */
#define NTP_SERVER "ntp.aliyun.com"
#define NTP_PORT 123
/* network information */
wiz_NetInfo default_net_info = {
.mac = {0x00, 0x08, 0xdc, 0x12, 0x22, 0x12},
.ip = {192, 168, 1, 30},
.gw = {192, 168, 1, 1},
.sn = {255, 255, 255, 0},
.dns = {8, 8, 8, 8},
.dhcp = NETINFO_DHCP};
uint8_t ethernet_buf[ETHERNET_BUF_MAX_SIZE] = {0};
uint8_t api_server_ip[4] = {0}; /* API服务器IP地址 */
uint8_t ntp_server_ip[4] = {0}; /* NTP服务器IP地址 */
// 用户输入缓冲区
char user_input[256] = {0};
// 系统时间
volatile uint32_t system_time = 0;
volatile uint32_t last_ntp_update = 0;
// 看门狗初始化
void IWDG_Init(void) {
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);
IWDG_SetPrescaler(IWDG_Prescaler_256); // 40KHz / 256 = 156Hz (6.4ms)
IWDG_SetReload(156 * 10); // 10秒超时
IWDG_ReloadCounter();
IWDG_Enable();
}
/**
* @brief 自定义fgets函数,支持回显和退格
* @param str: 输入缓冲区
* @param size: 缓冲区大小
* @return 输入字符串指针
*/
char *custom_fgets(char *str, int size) {
int count = 0;
int c; // 改为int类型接收所有字节值
// 清空串口输入缓冲区
while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET) {
USART_ReceiveData(USART1);
}
while (count < size - 1) {
c = fgetc(stdin); // 获取字节(int类型)
// 回车结束输入
if (c == '\r' || c == '\n') {
if (count > 0) {
str[count] = '\0';
printf("\r\n");
return str;
}
continue;
}
// 退格处理
if (c == '\b') {
if (count > 0) {
count--;
printf("\b \b");
}
continue;
}
// 接受所有非控制字符 (包括UTF-8字节)
if (c != 0) { // 关键修改:允许所有非空字符
str[count++] = (char)c;
putchar(c);
}
}
str[count] = '\0';
printf("\r\n");
return str;
}
int main(void) {
uint16_t len;
delay_init();
debug_usart_init();
wiz_timer_init();
wiz_spi_init();
wiz_rst_int_init();
IWDG_Init(); // 初始化看门狗
printf("System Initialized\r\n");
printf("W5500 HTTP Client Example with Doubao AI\r\n");
/* wizchip init */
wizchip_initialize();
network_init(ethernet_buf, &default_net_info);
// 解析NTP域名
if (do_dns(ethernet_buf, (uint8_t *)NTP_SERVER, ntp_server_ip)) {
printf("DNS resolution failed for %s\r\n", NTP_SERVER);
} else {
printf("%s resolved to: %d.%d.%d.%d\r\n",
NTP_SERVER,
ntp_server_ip[0],
ntp_server_ip[1],
ntp_server_ip[2],
ntp_server_ip[3]);
// 获取NTP时间
if (get_ntp_time(ntp_server_ip, NTP_PORT, &system_time)) {
last_ntp_update = system_time;
printf("NTP time updated: %lu\r\n", system_time);
}
}
// 解析API域名
if (do_dns(ethernet_buf, (uint8_t *)API_DOMAIN, api_server_ip)) {
printf("DNS resolution failed for %s\r\n", API_DOMAIN);
while (1) {
IWDG_ReloadCounter(); // 喂狗
delay_ms(100);
}
}
printf("%s resolved to: %d.%d.%d.%d\r\n",
API_DOMAIN,
api_server_ip[0],
api_server_ip[1],
api_server_ip[2],
api_server_ip[3]);
printf("Network initialized. Type your question and press Enter:\r\n");
uint32_t last_time_check = system_time;
while(1) {
// 喂狗操作
IWDG_ReloadCounter();
// 更新时间(每秒更新)
if (system_time - last_time_check >= 1) {
system_time++;
last_time_check = system_time;
// 每小时同步一次NTP
if ((system_time - last_ntp_update) >= 3600) {
if (get_ntp_time(ntp_server_ip, NTP_PORT, &system_time)) {
last_ntp_update = system_time;
printf("NTP time re-synced: %lu\r\n", system_time);
}
}
}
printf("User's question: ");
custom_fgets(user_input, sizeof(user_input));
// 移除所有换行符和回车符
char *src = user_input, *dst = user_input;
while (*src) {
if (*src != '\r' && *src != '\n') {
*dst++ = *src;
}
src++;
}
*dst = '\0';
if(strlen(user_input) > 0) {
printf("Sending to AI...\r\n");
// 构建并发送POST请求
len = http_post_for_doubao(ethernet_buf, user_input);
do_http_request(SOCKET_ID, ethernet_buf, len, api_server_ip, API_PORT);
// 清空输入缓存
memset(user_input, 0, sizeof(user_input));
}
delay_ms(100);
}
}
5.2 httpclient修改
httpclient.c文件修改如下:
http_post_for_doubao 函数负责构建符合豆包 AI API 规范的 HTTP POST 请求,核心功能包括:
- JSON 请求体构建:
- 使用 snprintf 生成符合 OpenAI 格式的 JSON 请求
- 包含 model 参数和 messages 数组,其中 messages 数组包含用户角色和内容
- 在 system 角色消息中添加当前时间(通过get_current_time_str获取)
- 示例生成的 JSON:
{"model":"ep-20241211221157-z2rfx","messages":[{"role":"user","content":"用户问题"}]}
- HTTP 请求构建:
- 请求行:指定 POST 方法和 API 路径
- 请求头:包含 Host、Authorization、Content-Type 等关键字段
- Content-Length 字段确保服务器正确解析请求体长度
- 使用 Connection: close 告知服务器请求完成后关闭连接
do_http_request 函数负责处理 HTTP 请求的发送和响应解析,是网络通信的核心部分:
1. 通信流程解析
- Socket 管理:
- 每次请求前关闭并重新打开 Socket,确保连接 freshness
- 使用 TCP 协议(Sn_MR_TCP)建立可靠连接
- 本地端口固定为 50000,可根据需要调整
- 状态机处理:
- 通过 getSn_SR 获取 Socket 状态,基于状态机处理不同情况
- SOCK_INIT:连接到服务器
- SOCK_ESTABLISHED:发送请求并处理响应
- SOCK_CLOSE_WAIT:处理服务器关闭连接前的剩余数据
- SOCK_CLOSED:重新打开 Socket
2. 响应解析逻辑
- JSON 响应处理:
- 检测 JSON 开始标记 '{'
- 查找 "content" 字段开始位置(通过字符匹配)
- 处理转义字符(如 "、\ 等)
- 遇到结束引号时停止解析内容
- 超时机制:
- 接收超时计数器 recv_timeout
- 超过 10 秒(10 次循环)判定为请求超时
- 超时后关闭连接并返回错误
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "socket.h"
#include "wizchip_conf.h"
#include "httpclient.h"
#include "wiz_platform.h"
#include "stm32f10x_iwdg.h"
#include "ntp_client.h"
// 豆包API参数声明
#define DOUBAO_API_KEY "dc2972ae-fc48-4ecf-b113-9dff3668e70c"
#define MODEL_ID "ep-20241211221157-z2rfx"
#define API_DOMAIN "ark.cn-beijing.volces.com"
#define API_PATH "/api/v3/chat/completions"
extern volatile uint32_t system_time; // 来自main.c的全局时间
/**
* @brief 构建豆包API的POST请求
* @param pkt: 数据缓冲区
* @param user_message: 用户消息
* @return 数据包长度
*/
uint32_t http_post_for_doubao(uint8_t *pkt, const char *user_message) {
char json_body[1024]; // 增大缓冲区
char time_str[32];
// 获取当前时间字符串
get_current_time_str(time_str, sizeof(time_str));
// 构建JSON请求体
snprintf(json_body, sizeof(json_body),
"{\"model\":\"%s\",\"messages\":["
"{\"role\":\"system\",\"content\":\"Current time is %s\"},"
"{\"role\":\"user\",\"content\":\"%s\"}"
"]}",
MODEL_ID, time_str, user_message);
int body_len = strlen(json_body);
// 清空缓冲区
memset(pkt, 0, ETHERNET_BUF_MAX_SIZE);
// 构建HTTP请求
char *buf = (char *)pkt;
int pos = 0;
// 请求行
pos += sprintf(buf + pos, "POST %s HTTP/1.1\r\n", API_PATH);
// 请求头
pos += sprintf(buf + pos, "Host: %s\r\n", API_DOMAIN);
pos += sprintf(buf + pos, "Authorization: Bearer %s\r\n", DOUBAO_API_KEY);
pos += sprintf(buf + pos, "Content-Type: application/json\r\n");
pos += sprintf(buf + pos, "Content-Length: %d\r\n", body_len);
pos += sprintf(buf + pos, "Connection: close\r\n");
pos += sprintf(buf + pos, "\r\n"); // 空行结束头部
// 请求体
pos += sprintf(buf + pos, "%s", json_body);
return pos; // 返回数据包长度
}
/**
* @brief HTTP响应处理
* @param data: HTTP响应数据
* @param len: 数据长度
*/
void process_http_response(uint8_t *data, uint16_t len) {
// 查找JSON正文开始位置
char *json_start = strstr((char *)data, "\r\n\r\n");
if (!json_start) {
json_start = strstr((char *)data, "\n\n");
}
if (json_start) {
json_start += 4; // 跳过空行
// 简化版JSON解析
char *content_start = strstr(json_start, "\"content\":\"");
if (content_start) {
content_start += 11; // 跳过"content":"
char *content_end = strstr(content_start, "\"");
if (content_end) {
printf("\r\nAI: ");
char *p = content_start;
while (p < content_end) {
// 处理转义字符
if (*p == '\\') {
p++;
if (*p == 'n') {
printf("\n");
} else if (*p == 't') {
printf("\t");
} else {
putchar(*p);
}
} else {
putchar(*p);
}
p++;
}
printf("\r\n");
return;
}
}
// 错误信息处理
char *error_start = strstr(json_start, "\"message\":\"");
if (error_start) {
error_start += 11; // 跳过"message":"
char *error_end = strstr(error_start, "\"");
if (error_end) {
printf("\r\nAI Error: ");
for (char *p = error_start; p < error_end; p++) {
putchar(*p);
}
printf("\r\n");
return;
}
}
}
printf("\r\nInvalid response format\r\n");
}
/**
* @brief HTTP Client get data stream test.
* @param sn: socket number
* @param buf: request message content
* @param len: request message length
* @param destip: destion ip
* @param destport: destion port
* @return 0:timeout,1:Received response..
*/
uint8_t do_http_request(uint8_t sn, uint8_t *buf, uint16_t len, uint8_t *destip, uint16_t destport) {
uint16_t local_port = 50000;
uint16_t recv_timeout = 0;
uint8_t send_flag = 0;
uint16_t total_len = 0;
uint8_t response_buf[ETHERNET_BUF_MAX_SIZE] = {0};
// 先关闭socket(如果已打开),然后重新打开
close(sn);
socket(sn, Sn_MR_TCP, local_port, 0x00);
while (1) {
// 喂狗操作
IWDG_ReloadCounter();
switch (getSn_SR(sn)) {
case SOCK_INIT:
// Connect to http server.
connect(sn, destip, destport);
break;
case SOCK_ESTABLISHED:
if (send_flag == 0) {
// send request
send(sn, buf, len);
send_flag = 1;
}
// Response content processing
len = getSn_RX_RSR(sn);
if (len > 0) {
// 读取数据
uint16_t read_len = recv(sn, response_buf + total_len,
ETHERNET_BUF_MAX_SIZE - total_len - 1);
total_len += read_len;
// 检查是否接收完成(根据Content-Length或连接关闭)
if (getSn_SR(sn) == SOCK_CLOSE_WAIT || total_len >= ETHERNET_BUF_MAX_SIZE - 1) {
// 处理完整响应
response_buf[total_len] = '\0';
process_http_response(response_buf, total_len);
disconnect(sn);
close(sn);
return 1;
}
} else {
recv_timeout++;
delay_ms(100);
}
// timeout handling
if (recv_timeout > 100) { // 10秒超时
printf("Request failed: Timeout!\r\n");
disconnect(sn);
close(sn);
return 0;
}
break;
case SOCK_CLOSE_WAIT:
// 读取剩余数据
len = getSn_RX_RSR(sn);
if (len > 0) {
uint16_t read_len = recv(sn, response_buf + total_len,
ETHERNET_BUF_MAX_SIZE - total_len - 1);
total_len += read_len;
}
// 处理完整响应
response_buf[total_len] = '\0';
process_http_response(response_buf, total_len);
disconnect(sn);
close(sn);
return 1;
case SOCK_CLOSED:
// close socket
close(sn);
// open socket
socket(sn, Sn_MR_TCP, local_port, 0x00);
break;
default:
break;
}
}
}
5.3 接入NTP服务
下面通过接入阿里云的NTP服务器可通过豆包AI获取时间。
需要加入ntp_client.c文件
1. get_ntp_time 函数
该函数的主要功能是从 NTP 服务器获取时间戳,并将其转换为 Unix 时间格式。
- NTP 请求包构建:创建一个标准 NTP 请求包,设置版本为 4,模式为客户端(Mode=3)。
- 网络通信处理:使用 UDP 协议与 NTP 服务器通信,通过 WIZnet 芯片的 socket API 实现。
- 超时处理机制:设置了 5 秒的超时限制,防止程序长时间阻塞。
- 时间戳提取转换:从 NTP 响应包的特定位置(第 40-43 字节)提取时间信息,并转换为 Unix 时间戳(自 1970 年 1 月 1 日以来的秒数)。
2. get_current_time_str 函数
该函数将系统时间转换为人类可读的时间字符串(格式:HH:MM:SS)。
- 时区处理:通过GMT_OFFSET_SEC参数调整时区。
- 时间计算:将总秒数转换为小时、分钟和秒的格式。
- 字符串格式化:使用snprintf确保输出字符串不会溢出缓冲区。
ntp_client.c如下:
#include "ntp_client.h"
#include "socket.h"
#include "wizchip_conf.h"
#include <string.h>
#include <stdio.h> // 添加snprintf声明
// NTP时间戳起始点 (1900-01-01 到 1970-01-01 的秒数)
#define NTP_TIMESTAMP_DELTA 2208988800UL
/**
* @brief 从NTP服务器获取时间
* @param ntp_server: NTP服务器IP
* @param port: NTP端口
* @param timestamp: 返回的时间戳
* @return 成功返回1,失败返回0
*/
uint8_t get_ntp_time(uint8_t *ntp_server, uint16_t port, volatile uint32_t *timestamp) {
uint8_t sock = NTP_SOCKET_ID;
uint8_t ntp_packet[48] = {0};
uint8_t response[48] = {0};
uint16_t len;
uint32_t timeout = 0;
// 创建NTP请求包
memset(ntp_packet, 0, sizeof(ntp_packet));
ntp_packet[0] = 0x1B; // LI=0, Version=4, Mode=3 (Client)
// 关闭socket(如果已打开)
close(sock);
// 创建UDP socket
if (socket(sock, Sn_MR_UDP, 0, 0) != sock) {
return 0;
}
// 发送NTP请求
sendto(sock, ntp_packet, sizeof(ntp_packet), ntp_server, port);
// 等待响应
while (timeout++ < 1000) { // 5秒超时
if ((len = getSn_RX_RSR(sock)) > 0) {
if (len > sizeof(response)) len = sizeof(response);
len = recvfrom(sock, response, len, ntp_server, &port);
if (len >= 48) {
// 提取时间戳 (第40-43字节)
uint32_t ntp_time = (uint32_t)response[40] << 24;
ntp_time |= (uint32_t)response[41] << 16;
ntp_time |= (uint32_t)response[42] << 8;
ntp_time |= (uint32_t)response[43];
// 转换为Unix时间戳
*timestamp = ntp_time - NTP_TIMESTAMP_DELTA;
close(sock);
return 1;
}
}
delay_ms(5);
}
close(sock);
return 0;
}
/**
* @brief 获取当前时间字符串
* @param buffer: 输出缓冲区
* @param size: 缓冲区大小
*/
void get_current_time_str(char *buffer, size_t size) {
uint32_t current = system_time + GMT_OFFSET_SEC;
uint32_t seconds = current % 86400; // 删除未使用的days变量
uint8_t hour = seconds / 3600;
uint8_t minute = (seconds % 3600) / 60;
uint8_t second = seconds % 60;
snprintf(buffer, size, "%02d:%02d:%02d", hour, minute, second);
}
6 对话测试
硬件连接完毕,烧录程序上电打印如下信息:
7 总结
本文介绍基于W5100Sio-M模块和STM32F103VCT6实现豆包AI接入方案。含硬件连接、豆包API参数获取,修改例程实现HTTP通信,通过DNS解析域名、构建POST请求,经串口交互发送问题并解析AI响应,完成物联网设备与豆包AI的HTTP协议通信。感谢大家的耐心阅读!如果您在阅读过程中有任何疑问,或者希望进一步了解这款产品及其应用,欢迎随时通过私信或评论区留言。我们会尽快回复您的消息,为您提供更详细的解答和帮助!
更多推荐
所有评论(0)