Bài 50.328 phút đọc
Đo nhiệt độ và số học dấu chấm cố định
Sau bài này bạn sẽ làm được
- Đổi giá trị ADC sang mili vôn và độ C mà không dùng số thực
- Hiểu định dạng Q và chọn số bit phần thập phân
- Cài bộ lọc trung bình trượt và bộ lọc thông thấp một cực
- Ước lượng sai số tích lũy của phép chia số nguyên
Cảm biến cho ra số nguyên, và người dùng muốn thấy 24,97 độ C. Giữa hai đầu đó là một chuỗi phép tính mà nếu làm bằng số thực thì tốn Flash và chu kỳ, còn nếu làm ẩu bằng số nguyên thì sai lệch dần theo thời gian. Bài này chỉ ra cách làm đúng.
#Vì sao tránh số thực
| Phép tính | Cortex-M0+ | Cortex-M4F | Cortex-M4F double |
|---|---|---|---|
| Cộng số nguyên 32 bit | 1 chu kỳ | 1 chu kỳ | - |
| Nhân số nguyên 32 bit | 1 chu kỳ | 1 chu kỳ | - |
| Chia số nguyên 32 bit | khoảng 30 tới 100 (gọi thư viện) | 2 tới 12 chu kỳ | - |
| Cộng float | khoảng 100 (phần mềm) | 1 chu kỳ | khoảng 100 |
| Nhân float | khoảng 130 (phần mềm) | 1 chu kỳ | khoảng 150 |
| Chia float | khoảng 300 (phần mềm) | 14 chu kỳ | khoảng 400 |
terminal
arm-none-eabi-gcc -Wdouble-promotion -c do-nhiet.c
do-nhiet.c:14:29: warning: implicit conversion from 'float' to 'double' to match other operand of binary expression [-Wdouble-promotion]
14 | float u = raw * 3.3 / 4095.0f;
| ^~~| Cách làm | Flash thêm | Ghi chú |
|---|---|---|
| Chỉ số nguyên | 0 | Không kéo thêm gì |
| float với FPU phần cứng | khoảng 1 KB | Vài hàm hỗ trợ đổi kiểu |
| float phần mềm | khoảng 4 tới 6 KB | Cả bộ libgcc số thực |
| printf có %f | khoảng 8 tới 12 KB nữa | Bộ định dạng số thực rất nặng |
#Từ giá trị ADC sang mili vôn
adc-doi.c
#include <stdint.h>
#define ADC_TOI_DA 4095u /* ADC 12 bit: 0 tới 4095 */
#define VREF_MV 3300u /* điện áp tham chiếu, mili vôn */
/* Đổi giá trị ADC sang mili vôn, làm tròn tới số gần nhất. */
uint16_t adc_sang_mv(uint16_t raw)
{
/* raw * 3300 lớn nhất là 13 513 500, cần 32 bit */
uint32_t tu = (uint32_t)raw * VREF_MV;
return (uint16_t)((tu + ADC_TOI_DA / 2u) / ADC_TOI_DA);
}Gộp cả chuỗi phép tính thành một
Cảm biến nhiệt độ trong STM32F4 cho ra điện áp 760 mV ở 25 độ C và thay đổi 2,5 mV mỗi độ. Cách ngây thơ là đổi sang mili vôn rồi mới tính nhiệt độ, nhưng như vậy mất phần lẻ ngay ở bước đầu.
Chia rồi mới nhân
int32_t nhiet_centi(uint16_t raw)
{
uint16_t mv = adc_sang_mv(raw); /* MAT PHAN LE O DAY */
/* 2.5 mV moi do => 40 centi do moi mV */
return ((int32_t)mv - 760) * 40 + 2500;
}
/* Do phan giai bi ket o 40 centi do = 0.4 do C.
Ket qua chi nhay theo buoc 0.4 do, du ADC min hon nhieu. */Nhân hết rồi chia một lần
int32_t nhiet_centi(uint16_t raw)
{
/* raw * 3300 / 4095 * 40 - 760*40 + 2500
= raw * 132000 / 4095 - 27900
4095 * 132000 = 540 540 000 < 2^31, khong tran */
return (int32_t)((uint32_t)raw * 132000u / 4095u) - 27900;
}terminal
./thu-nhiet
raw mV that T that chia som gop lai 943 759.93 24.971 24.60 24.97 975 785.71 35.286 35.00 35.28 1000 805.86 43.344 43.40 43.34 1024 825.20 51.081 51.00 51.08
#Định dạng Q
Định dạng Q
Cách biểu diễn số thực bằng số nguyên:
Qm.n nghĩa là dùng m bit cho phần nguyên và n bit cho phần lẻ. Giá trị thật bằng số nguyên lưu trong biến chia cho 2^n.| Định dạng | Kiểu chứa | Dải giá trị | Độ phân giải | Dùng cho |
|---|---|---|---|---|
| Q8.8 | int16_t | -128,000 tới 127,996 | 0,00391 | Nhiệt độ, phần trăm |
| Q16.16 | int32_t | -32768,0 tới 32767,99998 | 0,0000153 | Bộ lọc, tọa độ |
| Q4.12 | int16_t | -8,0 tới 7,99976 | 0,000244 | Hệ số lọc chuẩn hóa |
| Q0.31 | int32_t | -1,0 tới 0,99999999953 | 0,00000000047 | Tín hiệu âm thanh đã chuẩn hóa |
q16.h
#ifndef Q16_H
#define Q16_H
#include <stdint.h>
typedef int32_t Q16; /* Q16.16 */
#define Q16_MOT (1 << 16)
#define Q16_TU_INT(x) ((Q16)(x) << 16)
#define Q16_SANG_INT(q) ((int32_t)(q) >> 16)
static inline Q16 q16_cong(Q16 a, Q16 b) { return a + b; }
static inline Q16 q16_tru (Q16 a, Q16 b) { return a - b; }
/* Nhân: hai số Q16.16 nhân nhau ra Q32.32, phải dịch phải 16 */
static inline Q16 q16_nhan(Q16 a, Q16 b)
{
return (Q16)(((int64_t)a * (int64_t)b) >> 16);
}
/* Chia: dịch trái số bị chia 16 bit TRƯỚC khi chia */
static inline Q16 q16_chia(Q16 a, Q16 b)
{
return (Q16)((((int64_t)a) << 16) / b);
}
#endifKhi nào cần định dạng Q và khi nào không
- Không cần nếu bạn chỉ đổi đơn vị một lần, như từ ADC sang mili vôn. Nhân rồi chia một lần là đủ, và dễ đọc hơn.
- Cần nếu có phép tính lặp lại nhiều lần trên cùng biến, như bộ lọc, bộ tích phân PID, hay tính vị trí từ vận tốc. Ở đó phần lẻ phải được giữ lại giữa các lần lặp.
- Cần nếu hệ số nhân nhỏ hơn 1. Số nguyên không biểu diễn được 0,3 nên phải nhân với
0,3 * 65536 = 19661rồi dịch phải 16.
#Trung bình trượt và thông thấp một cực
Trung bình trượt cửa sổ lũy thừa của hai
tb-truot.c
#include <stdint.h>
#define CUA_SO 8u /* PHẢI là lũy thừa của 2 */
#define DICH 3u /* log2(CUA_SO) */
typedef struct {
uint16_t vong[CUA_SO];
uint32_t tong;
uint8_t vi;
uint8_t da_day;
} TbTruot;
void tb_khoi_tao(TbTruot *t, uint16_t gia_tri_dau)
{
for (uint8_t i = 0; i < CUA_SO; ++i) t->vong[i] = gia_tri_dau;
t->tong = (uint32_t)gia_tri_dau * CUA_SO; /* nạp sẵn, tránh vọt đầu */
t->vi = 0u;
t->da_day = 1u;
}
uint16_t tb_them(TbTruot *t, uint16_t x)
{
t->tong -= t->vong[t->vi]; /* bỏ mẫu cũ nhất */
t->vong[t->vi] = x;
t->tong += x; /* thêm mẫu mới */
t->vi = (uint8_t)((t->vi + 1u) & (CUA_SO - 1u));
return (uint16_t)(t->tong >> DICH);
}Thông thấp một cực
loc-mot-cuc.c
#include <stdint.h>
/* y[n] = y[n-1] + (x[n] - y[n-1]) / 2^K
Chỉ cần MỘT biến trạng thái, không cần mảng.
Nhưng phải giữ phần lẻ, nếu không bộ lọc sẽ dừng sớm. */
#define K 4u /* hệ số làm mượt */
typedef struct {
int32_t tich_luy; /* giá trị nhân với 2^K, tức Q(K) */
} LocMotCuc;
void loc_khoi_tao(LocMotCuc *l, int32_t gia_tri_dau)
{
l->tich_luy = gia_tri_dau << K;
}
int32_t loc_them(LocMotCuc *l, int32_t x)
{
l->tich_luy += ((x << K) - l->tich_luy) >> K;
return l->tich_luy >> K;
}| K | Hệ số | Hằng thời gian (mẫu) | Số mẫu tới 95 phần trăm | Tần số cắt khi lấy mẫu 100 Hz |
|---|---|---|---|---|
| 1 | 1/2 | 1,44 | 5 | 11,03 Hz |
| 2 | 1/4 | 3,48 | 11 | 4,58 Hz |
| 3 | 1/8 | 7,49 | 23 | 2,13 Hz |
| 4 | 1/16 | 15,49 | 47 | 1,03 Hz |
| 5 | 1/32 | 31,50 | 95 | 0,51 Hz |
| 6 | 1/64 | 63,50 | 191 | 0,25 Hz |
| 8 | 1/256 | 255,50 | 766 | 0,06 Hz |
#Sai số tích lũy của phép chia số nguyên
Một máy đo quãng đường đếm xung từ encoder. Mỗi xung ứng với 7/3 mili mét. Cách viết ngây thơ mất một phần bảy quãng đường sau ba trăm xung.
Chia mỗi lần, vứt phần dư
uint32_t quang_duong_mm = 0;
void moi_xung(void)
{
quang_duong_mm += 7u / 3u; /* 7/3 = 2, mat 0.333 mm moi xung */
}
/* Sau 300 xung: 600 mm.
Dung phai la 700 mm.
Thieu 100 mm, tuc 14.3 phan tram. */Mang phần dư sang lần sau
uint32_t quang_duong_mm = 0;
uint32_t du = 0;
void moi_xung(void)
{
du += 7u;
quang_duong_mm += du / 3u;
du %= 3u; /* GIU LAI phan du cho lan sau */
}
/* Sau 300 xung: 700 mm. Dung chinh xac.
Sai so khong bao gio vuot 1 mm, va khong tich luy. */terminal
./thu-encoder
so xung vut du mang du dung
30 60 70 70
300 600 700 700
3000 6000 7000 7000
30000 60000 70000 70000
Sai so cua cach vut du tang tuyen tinh theo so xung.#In số dấu chấm cố định
in-centi.c
#include <stdint.h>
#include "uart.h"
/* In giá trị centi độ, ví dụ 2497 thành "24.97" */
void in_centi(int32_t v)
{
if (v < 0) {
uart_gui_byte('-');
v = -v;
}
char dem[12];
uint8_t n = 0u;
uint32_t phn = (uint32_t)v / 100u;
uint32_t le = (uint32_t)v % 100u;
if (phn == 0u) dem[n++] = '0';
while (phn > 0u) {
dem[n++] = (char)('0' + phn % 10u);
phn /= 10u;
}
while (n > 0u) uart_gui_byte((uint8_t)dem[--n]);
uart_gui_byte('.');
uart_gui_byte((uint8_t)('0' + le / 10u));
uart_gui_byte((uint8_t)('0' + le % 10u));
}terminal
./thu-in
2497 -> 24.97
100 -> 1.00
5 -> 0.05
0 -> 0.00
-5 -> -0.05
-350 -> -3.50
-4012 -> -40.12Tự làm thử
- Viết
adc_sang_mvcó làm tròn và so sánh kết quả với bảng ở trên cho các giá trị 0, 1024, 2048, 3000, 4095. - Gộp chuỗi tính nhiệt độ thành một phân số duy nhất và kiểm tra tử số lớn nhất không tràn
uint32_t. - Cài bộ lọc thông thấp một cực có bộ tích lũy, rồi bỏ bộ tích lũy đi và đo xem nó dừng ở đâu với đầu vào 1000 và
K = 4. - Cài bộ đếm quãng đường với hệ số 7/3, chạy 3000 xung theo cả hai cách và so sánh kết quả.
- Viết hàm in số centi hỗ trợ số âm, thử với 0, 5, -5, -350 và
INT32_MIN.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Trên Cortex-M4F chỉ
floatchạy trên FPU, còndoublechạy bằng phần mềm. Bật-Wdouble-promotionđể bắt lỗi quên hậu tốf. - Nhân hết rồi chia một lần ở cuối, và luôn kiểm tử số lớn nhất có tràn hay không trước khi làm vậy.
- Định dạng
Qm.nlưu số thực trong số nguyên. Nhân hai sốQ16.16phải quaint64_trồi dịch phảinbit. - Bộ lọc thông thấp một cực phải giữ
Kbit lẻ trong bộ tích lũy, nếu không nó dừng cách giá trị đúng tới2^K - 1đơn vị. - Khi cộng dồn một phân số nhiều lần, giữ lại phần dư bằng
du %= bđể sai số không tích lũy tuyến tính.