
ST67W611M1 T01 HTTPS 實戰
前言
本篇繼續使用 STM32H7R7 + ST67W611M1 T01 + FreeRTOS,將實際網路需求拆成兩部分:
- 上傳: 使用 TLS Socket 向 HTTPS API 發送 JSON
POST,確認 HTTP 與業務層回應。 - 下載: 使用 HTTPS
GET取得 Manifest,再以 HTTPRange分段抓取 Binary,完成 ETag、長度、SHA-256 / Image 驗證。
上傳與下載流程
STM32H7R7 App
│
├── 取得 Wi-Fi IP(上一篇)
│
├── 上傳 JSON
│ ├─ DNS 解析 Server
│ ├─ 建立 TLS 1.2 Socket
│ ├─ 設定 CA / SNI / ALPN
│ ├─ 連線 TCP/443 + TLS
│ ├─ HTTP POST + JSON Body
│ ├─ 接收 HTTP 回應並解析 JSON
│ └─ Close / release credential
│
└── 下載 Binary
├─ HTTP GET 取得 Manifest
├─ 解析 URL、size、SHA-256 等
├─ 重複 HTTP GET + Range
├─ 逐段寫入 PSRAM staging
├─ 檢查 206 / Content-Range / ETag
├─ 計算整體 SHA-256 與映像有效性
└─ 通過驗證後交給後續 OTA 流程
上傳資料:HTTPS POST JSON
上傳使用 T01 的 W6X_Net_* Socket API。由於應用要帶自訂 X-Device-Type、X-Source 等 HTTP header,因此採用自己組 HTTP/1.1 request
Step 1:DNS → TLS Socket → CA / SNI / ALPN
/* 原始專案 https_client.c 的核心 API 呼叫順序。 */
uint8_t ip[4] = {0};
int32_t sock = -1;
uint8_t tag;
struct sockaddr_in addr = {0};
if (W6X_Net_ResolveHostAddress(HTTPS_HOST, ip) != W6X_STATUS_OK)
goto fail;
sock = W6X_Net_Socket(AF_INET, SOCK_STREAM, IPPROTO_TLS_1_2);
if (sock < 0) goto fail;
tag = (uint8_t)sock;
/* demo_ca_pem / demo_ca_pem_len 由應用程式提供。 */
if (W6X_Net_TLS_Credential_AddByContent(
tag, W6X_NET_TLS_CREDENTIAL_CA_CERTIFICATE,
"demo_ca.pem", demo_ca_pem, demo_ca_pem_len) != 0)
goto fail;
if (W6X_Net_Setsockopt(sock, SOL_TLS, TLS_SEC_TAG_LIST,
&tag, sizeof(tag)) != 0) goto fail;
if (W6X_Net_Setsockopt(sock, SOL_TLS, TLS_HOSTNAME,
HTTPS_HOST, strlen(HTTPS_HOST)) != 0) goto fail;
if (W6X_Net_Setsockopt(sock, SOL_TLS, TLS_ALPN_LIST,
"http/1.1", strlen("http/1.1")) != 0) goto fail;
addr.sin_family = AF_INET;
addr.sin_port = PP_HTONS(443);
addr.sin_addr.s_addr = ATON(ip);
if (W6X_Net_Connect(sock, (struct sockaddr *)&addr,
sizeof(addr)) != 0) goto fail;
/* 成功後開始 HTTP send / recv。
fail: 路徑必須 Close socket 並釋放已註冊 credential。 */
HTTPS_HOST、demo_ca_pem 與 demo_ca_pem_len 是本文公開示意的占位符,並不是 ST SDK 自帶的全域變數。實際 OGA 程式使用專案自己的 Server macro 及 CA PEM 陣列。若要完整部署,應加入 socket timeout、credential_added 旗標與所有錯誤出口清理。TLS_HOSTNAME 提供伺服器名稱,但不可單憑該設定就宣稱憑證鏈與 hostname 已通過驗證,必須實測驗證失效/錯誤憑證時會拒絕連線
Step 2:建立 HTTP POST 與 JSON Body
HTTP request header 要與 Body 明確分開,Content-Length 是Body 位元組數,不是 sizeof 整個 buffer
#define HTTPS_HOST "api.example.com" /* 公開示意:替換自己的 API Server */
#define HTTPS_PATH "/v1/events"
static const char json_body[] =
"{\"deviceId\":\"demo-001\",\"event\":\"startup\"}";
char header[384];
int n = snprintf(header, sizeof(header),
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
"Content-Type: application/json\r\n"
"Accept: application/json\r\n"
"Accept-Encoding: identity\r\n"
"Content-Length: %u\r\n"
"Connection: close\r\n\r\n",
HTTPS_PATH, HTTPS_HOST, (unsigned)(sizeof(json_body) - 1U));
if (n <= 0 || (size_t)n >= sizeof(header)) {
/* Header 超出 buffer:停止傳送 */
return -1;
}
/* send_all 是本專案封裝,用來處理 W6X_Net_Send() 的 partial send。 */
if (send_all(sock, header, (size_t)n) != 0 ||
send_all(sock, json_body, sizeof(json_body) - 1U) != 0) {
/* 回應未確認,不能判定 API 已寫入成功 */
return -1;
}
其中 send_all() 的核心寫法如下(實際應加上整體 deadline,避免卡在持續回傳 0 的情況):
static int send_all(int32_t sock, const char *data, size_t length)
{
while (length > 0U) {
ssize_t sent = W6X_Net_Send(sock, data, length, 0);
if (sent <= 0 || (size_t)sent > length) return -1;
data += sent;
length -= (size_t)sent;
}
return 0;
}
Step 3:讀取 HTTP Response 並確認成功
W6X_Net_Send() 成功不等於 Server 已接受資料。 最低限度應解析:HTTP status、完整的 Response Body,以及應用 API 自己定義的成功條件
/* 取自 https_client.c 的核心控制流程;
response buffer、timeout、HTTP_ResponseDecode 均為專案自備。 */
ssize_t count = W6X_Net_Recv(sock, response + received,
response_capacity - received, 0);
if (count > 0)
received += (size_t)count;
int parsed = HTTP_ResponseDecode(response, received,
count < 0, &http_status,
&body, &body_length);
if (parsed > 0) {
/* 實際專案還會解析 JSON status / data.inserted。 */
if (http_status == 200)
LogInfo("HTTP 200 received; validate business payload next\n");
}
Note:回應除了 HTTP 200,還要求 JSON 中的 status == 0、data.inserted == true,才視為上傳成功
下載資料:HTTPS GET
下載先從最基本的 GET 開始;小型 JSON 可以完整累積在固定 buffer,再做格式解析。大型 Binary 則不適合整包塞進 SRAM,而應分段處理
Step 1:GET 取得 Manifest
int n = snprintf(request_text, sizeof(request_text),
"GET %s HTTP/1.1\r\n"
"Host: %s\r\n"
"Accept: application/json\r\n"
"Accept-Encoding: identity\r\n"
"Connection: close\r\n\r\n",
url->path, url->host);
/* 然後以 W6X_Net_Send() 傳送,W6X_Net_Recv() 累積 HTTP header/body。
專案使用 BWIFI_HttpHeader() / BWIFI_HttpFeed() 進行邊界檢查。
取回 Manifest 後,由 BWIFI_ManifestParse() 驗證 URL、size、hash。 */
Step 2:用 HTTP Range 分段下載 Binary
推薦在 Server 支援 Range 且可保證檔案版本穩定時,利用 Range: bytes=start-end 分段下載
目前專案設定:
- 應用 HTTP Range:4096 bytes / request,定義於
BWIFI_RANGE_BYTES。 - STM32 一次
W6X_Net_Recv()的暫存 buffer:1024 bytes。 - HTTP Header buffer:2048 bytes。
- T01 NCP Receive buffer:9216 bytes(
W6X_NET_RECV_BUFFER_SIZE)。 - 本次測試專案的 OTA staging 配置上限為2 MiB;它不是模組對所有下載檔案的通用上限,也不能直接代表其他產品的 24 MB OTA 需求。
Range request 組裝與 Receive Loop
/* 這段位於 FetchRange() 中;
url、sock、offset、size、file_size、out、etag 等由外層提供。 */
int length = snprintf(request_text, sizeof(request_text),
"GET %s HTTP/1.1\r\nHost: %s\r\n"
"Accept-Encoding: identity\r\n"
"Range: bytes=%lu-%lu\r\n"
"%s%s%sConnection: close\r\n\r\n",
url->path, url->host,
(unsigned long)offset,
(unsigned long)(offset + size - 1U),
etag[0] ? "If-Range: " : "", etag,
etag[0] ? "\r\n" : "");
/* 實際程式依 length 逐步傳送,支援 partial send 與 deadline。 */
for (uint32_t sent = 0U; sent < (uint32_t)length;) {
ssize_t n = W6X_Net_Send(sock,
request_text + sent,
(uint32_t)length - sent, 0);
if (n < 0) goto failed;
if (n == 0) continue;
sent += (uint32_t)n;
}
/* Read path 使用 1024-byte scratch buffer,不能假設每次 Recv 都回 4096。 */
while (received < size) {
ssize_t n = W6X_Net_Recv(sock, rx, sizeof(rx), 0);
if (n == -2 || n == 0) {
/* 專案會延遲並持續檢查總 timeout */
continue;
}
if (n < 0 || n > (ssize_t)sizeof(rx)) goto failed;
/* 先收集 HTTP Header 並驗證,再把 Body 複製至 staging。 */
}
Step 3:每段驗證 + 最終 SHA-256
正確的 Range download 不只是反覆呼叫 W6X_Net_Recv()。每段還應檢查:
- HTTP status 是 206 Partial Content,而不是被 Server 忽略 Range 後回傳的 200。
Content-Range的起點、終點與總長度對應預期的檔案區間。Content-Length等於要求的區塊大小。- 整個下載使用同一個 ETag / 同一個檔案版本,避免新舊版本混合。
- 每段實際寫入的長度、PSRAM 讀回資料一致;下載完成後做 SHA-256 和映像檢查。
for (uint32_t pos = 0U; pos < image->size;) {
uint32_t n = image->size - pos;
if (n > BWIFI_RANGE_BYTES)
n = BWIFI_RANGE_BYTES;
if (!FetchRange(&url, ip, pos, n,
image->size, destination + pos))
return 0;
pos += n;
vTaskDelay(1U);
}
if (!HashMatches(destination, image->size, image->sha256)) {
LogError("[OTA] SHA256 mismatch\n");
return 0;
}
if (!BWIFI_ImageValid(destination, image->size)) {
LogError("[OTA] image validation failed\n");
return 0;
}
T01 的 6144-byte TLS 限制
ST 官方文件指出,T01 模組內 SSL socket 的 TLS plaintext fragment 上限為 6144 bytes。HTTP Range 的 4096 bytes 與 Recv() 的 1024 bytes 是應用端設定,不能證明任何 HTTPS Server 都會配合輸出相容的 TLS records。
| 狀況 | 說明 | 建議 |
|---|---|---|
| Server / TLS 行為相容 | T01 可逐段收資料,應用檔案可大於 6144 bytes | 驗證長時間下載、Range / ETag、timeout |
| TLS handshake 失敗 | 可能與 CA、hostname、cipher、TLS fragment、ALPN 或網路有關 | 先收集 TLS / socket error,而不是只縮小 Recv() |
| Range 不支援 | Server 回 200 而非 206 | 更換 API 策略或讓伺服器支援 Range |
| CDN 改寫壓縮或快取內容 | 可能造成 byte range 或 hash 不一致 | Accept-Encoding: identity、檔案版本鎖定、檢查 ETag |
| T01 TLS 相容性無法排除 | 無法控制 Server TLS 行為 | 評估 T02 Host LwIP / MbedTLS,並做 Host RAM / CPU / PSRAM 壓力測試 |
如果 Host 同時在使用 TouchGFX、PSRAM 及其他即時任務,不要把 T02 視為零成本替代方案。需要先測記憶體配置、Task stack / Heap、DMA / Cache、TLS 尖峰使用量及多次斷線後資源釋放。
