The W5100Sio-M module accesses Doubao AI via the HTTP protocol
This article introduces a solution for implementing Doubao AI access based on the W5100Sio-M module and STM32F103VCT6.
1. Introduction
HTTP (Hypertext Transfer Protocol) is an application-layer protocol used for distributed, collaborative, hypermedia information systems. It transmits data based on the TCP/IP communication protocol and is the foundation of data communication on the World Wide Web (WWW). HTTP was initially designed to provide a method for publishing and receiving HTML pages. Resources requested via HTTP or HTTPS are identified by Uniform Resource Identifiers (URIs). This is a brief introduction to the HTTP protocol. For a more in-depth understanding of this protocol, please refer to the introduction on the Mozilla website: HTTP Overview - HTTP | MDN
The W5100Sio-M is a high-performance SPI-to-Ethernet module launched by WISHI, with the following features:
Minimalist design: integrates MAC, PHY, 16KB cache and RJ45 network port, directly connects to the main controller via 4-wire SPI interface, 3.3V power supply, compact size suitable for embedded scenarios .
Simple and easy to use: Users no longer need to port the complex TCP/IP protocol stack to the MCU, and can directly develop based on application layer data.
Abundant resources: Provides a wealth of MCU application examples and hardware reference designs that can be directly referenced and used, greatly shortening the development time. Hardware is compatible with the W5500io-M module, facilitating solution development and iteration.
Wide range of applications: It is widely used in industrial control, smart grid, charging piles, security and fire protection, new energy, energy storage and other fields.
Product Link: Product Details
2. Project Environment
2.1 Hardware Environment
W5100Sio-M
STM32F103VCT6 EVB
Network cable
DuPont wires
Switch or router
2.2 Software Environment
Example program link : https://www.w5100S.com
Serial Port Assistant
Bean bag A P I
Alibaba Cloud NTP server address
Keil 5
3 Hardware Connection and Solution
3.1 W5100S Hardware Connection
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 Scheme Diagram
4. Obtaining AI parameters for Doubao
4.1 Activate Service
Go to Account Login - Volcano Engine , and follow the prompts to register or log in to an account.
Clicking the "Activate Service" button on the far right of Doubao-pro-4k will bring up a pop-up window.
Select the 6 models of Doubao in the pop-up window.
(Doubao-pro-4k, Doubao-pro-8k, Doubao-pro-32k, Doubao-lite-4k, Doubao-lite-8k, Doubao-lite-32k), then click [Activate Now].
After activating the service, each model can enjoy 500,000 free tokens per day (1 token is approximately 4 characters or 0.75 words).
4.2 Creating an API Key
Go to API Key Management , click 【Create API Key】, fill in the name, and create the API Key for later use.
4.3 Creating an Access Point
The Doubao model cannot be used directly; an access point must be created within the platform before it can be used.
Taking Doubao-pro-32k as an example: Go to Create Inference Access Point and click 【Create Inference Access Point】.
Clicking the "Add Model" button will bring up a pop-up window.
In the model plaza, select "Doubao-pro-32k", and the model version will appear on the right.
There is usually only one model version, and the name is a date consisting of 6 digits (e.g., 240515), but there may also be versions with prefixes (e.g., functioncall-240515, character-240528).
Choose a model version without a prefix, such as 240515, which only has 6 digits.
After selecting the model version, click "Add" in the bottom right corner of the page.
The name should be the text shown in the "Access Model" field, such as "Doubao-pro-32k-240515" (replace the forward slash / with a hyphen -).
Click the "Access Model" button on the right side of the page.
Then, you will return to the model inference page. At this time, you will see the access point you just created named "Doubao-pro-32k-240515" in the table. Below the name, there is a string of text starting with ep- and in the format ep-xxxxxxxxxx-xxxxx. This is the access point ID we need. Copy it for later use.
You can correctly call the Doubao API by obtaining the following parameters :
Enter https://ark.cn-beijing.volces.com/api/v3 in the API address (OPENAI_BASE_URL) .
Enter the API Key you created in the OPENAI_API_KEY field.
Enter the access point ID you created in the OPENAI_MODEL field.
5 routine modifications
5.1 Modification of the main function file
The main.c file has been modified as follows :
Main functions include
Main functions include:
Hardware initialization: Configure SPI interface, USART debug serial port, timers, and watchdog timer.
Network initialization: Configure W5100 network parameters (MAC, IP, gateway, etc.)
NTP Time Synchronization: Obtain network time from Alibaba Cloud NTP server
Domain name resolution: Resolving the API domain name into an IP address via DNS.
User interaction: Receive user questions via serial port and send them to 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 Modify HttpClient
The httpcl ient.c file has been modified as follows :
The `http_post_for_doubao` function is responsible for constructing HTTP POST requests that conform to the Doubao AI API specification. Its core functionalities include:
JSON request body construction:
Use snprintf to generate JSON requests conforming to OpenAI format.
It includes a `model` parameter and a `messages` array, where the `messages` array contains user roles and content.
Add the current time (obtained via get_current_time_str) to the system role message.
Example of generated JSON:
{"model":"ep-20241211221157-z2rfx","messages":[{"role":"user","content":"User Issues"}]}
HTTP request construction:
Request line: Specifies the POST method and API path
Request headers: contain key fields such as Host, Authorization, and Content-Type.
The Content-Length field ensures that the server correctly parses the request body length.
Use `Connection: close` to tell the server to close the connection after the request is complete.
The `do_http_request` function is responsible for handling the sending of HTTP requests and the parsing of responses, and is a core part of network communication.
1. Communication Flow Analysis
Socket Management:
Close and reopen the socket before each request to ensure connection freshness.
Establish a reliable connection using the TCP protocol (Sn_MR_TCP)
The local port is fixed at 50000 and can be adjusted as needed.
State machine processing:
The socket state is obtained through getSn_SR, and different situations are handled based on a state machine.
SOCK_INIT: Connect to the server
SOCK_ESTABLISHED: Sends a request and processes the response.
SOCK_CLOSE_WAIT: Handles remaining data before the server closes the connection.
SOCK_CLOSED: Reopen the socket.
2. Response parsing logicJSON response handling:
Detect the start tag '{' in JSON.
Find the starting position of the "content" field (by character matching).
Handling escape characters (such as ", \, etc.)
The parsing stops when a closing quotation mark is encountered.
Timeout mechanism:
Receive timeout counter recv_timeout
If the request takes more than 10 seconds (10 loops), it is considered a timeout.
The connection will be closed and an error will be returned after the timeout.
#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 Accessing NTP Service
The following method uses Doubao AI to obtain the time by connecting to Alibaba Cloud 's NTP server .
The ntp_client.c file needs to be included .
1. The get_ntp_time function
The main function of this function is to obtain a timestamp from an NTP server and convert it to a Unix time format.
NTP request packet construction: Create a standard NTP request packet, set the version to 4, and the mode to client (Mode=3).
Network communication processing: Communicates with the NTP server using the UDP protocol, implemented through the socket API of the WIZnet chip.
Timeout handling mechanism: A 5-second timeout limit is set to prevent the program from being blocked for a long time.
Timestamp Extraction and Conversion: Extract time information from a specific location (bytes 40-43) in the NTP response packet and convert it to a Unix timestamp (the number of seconds since January 1, 1970).
2. The `get_current_time_str` function
This function converts the system time into a human-readable time string (format: HH:MM:SS).
Time zone handling: Adjust the time zone using the GMT_OFFSET_SEC parameter.
Time calculation: Converts the total number of seconds to hours, minutes, and seconds.
String formatting: Use snprintf to ensure that the output string does not overflow the buffer.
The ntp_client.c file is as follows :
#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. Dialogue Test
After the hardware connection is complete, the following information will be printed after powering on the program:

