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 硬件环境

  1. W5100Sio-M
  2. STM32F103VCT6 EVB
  3. 网络连接线
  4. 杜邦线若干
  5. 交换机或路由器

2.2 软件环境

  1. 例程连接:https://www.w5100S.com
  2. 串口助手
  3. 豆包API
  4. 阿里云NTP服务器地址
  5. 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

5 例程修改

5.1 主函数文件修改

main.c文件修改如下

主要功能包括

主要功能包括:

  1. 硬件初始化:配置 SPI 接口、USART 调试串口、定时器和看门狗
  2. 网络初始化:设置 W5100 网络参数(MAC、IP、网关等)
  3. NTP 时间同步:从阿里云 NTP 服务器获取网络时间
  4. 域名解析:通过 DNS 将 API 域名解析为 IP 地址
  5. 用户交互:通过串口接收用户问题并发送给 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协议通信。感谢大家的耐心阅读!如果您在阅读过程中有任何疑问,或者希望进一步了解这款产品及其应用,欢迎随时通过私信或评论区留言。我们会尽快回复您的消息,为您提供更详细的解答和帮助!

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐