:查表+定時器觸發(fā)+DMA避坑指南)
簡介本資源是面向嵌入式初學(xué)者與STM32進(jìn)階開發(fā)者的DAC正弦波生成實踐項目聚焦STM32F103RCT6芯片的12位DAC外設(shè)應(yīng)用解決模擬信號發(fā)生器基礎(chǔ)功能實現(xiàn)問題適用于課程設(shè)計、實驗教學(xué)及小型信號源開發(fā)場景。壓縮包共76個文件以33個.h頭文件和32個.c源文件為主體涵蓋HAL庫驅(qū)動、定時器觸發(fā)配置、DMA數(shù)據(jù)傳輸、正弦波查表法實現(xiàn)及LED/USART等配套外設(shè)模塊另含Keil工程文件uvprojx/uvoptx、啟動代碼startup_stm32f10x_hd.s、調(diào)試配置JLinkSettings.ini及實測波形圖LED波形仿真.jpg結(jié)構(gòu)完整開箱即用。已有10335人學(xué)習(xí)下載。讀者可直接復(fù)現(xiàn)高精度正弦波輸出掌握DACTIMDMA協(xié)同工作機制獲取經(jīng)實測驗證的時序配置參數(shù)、濾波建議與常見干擾排查要點并參考清晰分層的目錄結(jié)構(gòu)HARDWARE/SYSTEM/USER/STM32F10x_FWLib快速理解工程組織邏輯。1. STM32F103用DAC功能輸出正弦波不是調(diào)個寄存器就出波形而是要掐準(zhǔn)時序、填對查表、繞開DMA雙緩沖陷阱你手頭有塊最常見的藍(lán) pillSTM32F103C8T6想用片上 DAC 輸出一個干凈的 1kHz 正弦波驅(qū)動運放或音頻小喇叭——結(jié)果燒錄后示波器一接波形歪斜、頻率漂移、甚至干脆沒輸出。這不是硬件壞了而是 DAC 在 STM32F103 上屬于“半托管外設(shè)”它不支持自動觸發(fā)自動更新自動翻轉(zhuǎn)的閉環(huán)流水線所有節(jié)奏都得你親手卡點。本資源是一套經(jīng)實測驗證的完整工程包含 Keil MDK 工程 CubeMX 配置導(dǎo)出 純 C 查表定時器觸發(fā)方案 DMA 單緩沖優(yōu)化版覆蓋從最簡寄存器直驅(qū)到工業(yè)級穩(wěn)定輸出的全路徑。適合剛焊完板子想快速驗證 DAC 功能的嵌入式新手也適合被 TIMDACDMA 三者時序?qū)Σ积R折磨過的中級工程師。它不講抽象原理只解決“為什么我填了 0x0FFF 還是沒電壓”“為什么用 SysTick 定時器波形就抖”“為什么 CubeMX 自動生成的 DAC 初始化進(jìn)不了中斷”這三類高頻翻車現(xiàn)場。2. DAC 基礎(chǔ)配置與正弦波生成邏輯為什么必須用查表法而非實時計算2.1 STM32F103 DAC 的硬件約束決定了查表是唯一可行路徑STM32F103 的 DAC 是 12 位電壓輸出型0–3.3V 對應(yīng) 0x000–0xFFF但沒有內(nèi)置波形發(fā)生器也不支持像高級芯片那樣設(shè)置“自動遞增地址自動觸發(fā)”。這意味著實時計算 sin(2πft) 需浮點運算ARM Cortex-M3 不帶 FPU單次計算耗時 30μs實測 Keil -O2 下sin()庫函數(shù)約 34μs根本無法支撐 10kHz 以上采樣率若強行用整數(shù)近似如查表插值代碼體積和分支判斷反而比直接查表更大DAC 數(shù)據(jù)寄存器DHRx寫入后需等待DAC 轉(zhuǎn)換時間典型 1μs 輸出建立時間典型 5μs才能穩(wěn)定期間若重復(fù)寫入會丟點。提示官方 RM0008 手冊第 19.4.4 節(jié)明確指出“DAC 的數(shù)據(jù)更新由軟件寫入 DHRx 觸發(fā)無硬件自動刷新機制”。這是所有翻車的根源——你必須自己保證寫入節(jié)奏嚴(yán)格等間隔。2.2 正弦波查表256 點夠用但起始相位和歸一化必須手算我們采用 256 點查表兼顧內(nèi)存占用與波形平滑度公式為dac_value[i] (uint16_t)(2047.0 * (1.0 sin(2.0 * PI * i / 256.0)) 0.5);為什么是2047.0因為 12 位 DAC 中心值為 20480x800但sin()輸出范圍是 [-1,1]加 1 后變?yōu)?[0,2]再乘 2047 得到 [0,4094] —— 這會溢出正確做法是縮放到 [0,4095] 的 50% 動態(tài)范圍dac_value[i] (uint16_t)(2047.0 * (1.0 sin(...)) 0.5);→ 實際輸出范圍 0x000–0xFFF中心在 0x800。為什么加0.5避免 C 語言截斷導(dǎo)致的系統(tǒng)性負(fù)偏實測不加會導(dǎo)致波形底部削頂。以下為生成表的 Python 腳本可直接運行生成sine_table.himport math TABLE_SIZE 256 with open(sine_table.h, w) as f: f.write(#ifndef SINE_TABLE_H\n#define SINE_TABLE_H\n\n) f.write(fconst uint16_t sine_table[{TABLE_SIZE}] {{\n) for i in range(TABLE_SIZE): # 歸一化到 0-4095中心 2048 val int(2047.0 * (1.0 math.sin(2.0 * math.pi * i / TABLE_SIZE)) 0.5) val max(0, min(4095, val)) # 保險鉗位 f.write(f 0x{val:03X}) if i TABLE_SIZE - 1: f.write(,) if i % 8 7: # 每行 8 個值提升可讀性 f.write(\n) else: f.write( ) f.write(};\n\n#endif // SINE_TABLE_H)邏輯說明該腳本生成的是標(biāo)準(zhǔn) C 頭文件sine_table[0] 0x800對應(yīng) sin(0)0sine_table[64] 0xFFF對應(yīng) sin(π/2)1sine_table[128] 0x800sin(π)0完全匹配 DAC 的 0–3.3V 輸出映射。參數(shù)TABLE_SIZE可改為 512 或 1024但需同步調(diào)整定時器重裝載值見第 3 章。2.3 DAC 模塊初始化避開 HAL 庫的“自動使能”陷阱HAL 庫的HAL_DAC_Start()默認(rèn)開啟 DAC 通道并啟動轉(zhuǎn)換但它不檢查 GPIO 是否已配置為模擬輸入模式。F103 的 DAC_OUT 引腳PA4/PA5若未手動設(shè)為ANALOG輸出將始終為 0V。這是新手第一大坑。正確初始化順序必須是使能 GPIOA 時鐘將 PA4/PA5 模式設(shè)為GPIO_MODE_ANALOG非AF_PP使能 DAC 時鐘調(diào)用HAL_DAC_Init()最后調(diào)用HAL_DAC_Start()。CubeMX 生成的代碼常把第 2 步漏掉默認(rèn)生成GPIO_MODE_AF_PP需手動修改MX_GPIO_Init()函數(shù)中對應(yīng)引腳配置// 錯誤CubeMX 默認(rèn)生成導(dǎo)致無輸出 GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; // 正確必須改為 ANALOG 模式 GPIO_InitStruct.Mode GPIO_MODE_ANALOG; // 關(guān)鍵 GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);參數(shù)說明GPIO_MODE_ANALOG是 DAC 輸出的強制要求任何其他模式包括復(fù)用推挽AF_PP都會使 DAC 內(nèi)部開關(guān)斷開輸出懸空。此設(shè)置與 ADC 輸入共用同一套模擬通路不可省略。3. 定時器觸發(fā) DAC 更新TIM2 主動喂數(shù)拒絕 SysTick 搶占失真3.1 為什么不能用 SysTick 或普通延時時序精度差 10 倍SysTick 默認(rèn) 1ms 中斷若用于 10kHz 波形100μs/點需在中斷里做for(i0;i10;i)循環(huán)寫 DAC —— 但每次寫寄存器耗時約 0.5μs10 次即 5μs加上中斷進(jìn)出開銷約 12μs實際間隔變成 117μs頻率偏差達(dá) 17%。更致命的是SysTick 可能被更高優(yōu)先級中斷搶占導(dǎo)致某次寫入延遲 200μs波形直接撕裂。3.2 TIM2 定時器觸發(fā) DAC硬件級等間隔保障STM32F103 支持“定時器更新事件 → DAC 觸發(fā)轉(zhuǎn)換”全程硬件鏈路零軟件干預(yù)。配置要點選擇 TIM2高級定時器 TIM1 不推薦因 F103C8T6 的 TIM1 通道有限且易與 PWM 沖突TIM2 時鐘源為 APB136MHz預(yù)分頻器PSC35→ 計數(shù)時鐘 1MHz自動重裝載值A(chǔ)RR99→ 更新事件周期 (991)/1MHz 100μs對應(yīng) 10kHz 采樣率DAC 觸發(fā)源設(shè)為DAC_TRIGGER_T2_TRGOTIM2 的 TRGO 信號DAC 工作在“硬件觸發(fā)”模式DAC_TriggerDAC_TRIGGER_T2_TRGO。CubeMX 配置關(guān)鍵參數(shù)Keil 工程中MX_DAC_Init()和MX_TIM2_Init()// MX_DAC_Init() 中關(guān)鍵設(shè)置 hdac.Instance DAC; if (HAL_DAC_Init(hdac) ! HAL_OK) { Error_Handler(); } DAC_ChannelConfTypeDef sConfig {0}; sConfig.DAC_Trigger DAC_TRIGGER_T2_TRGO; // 必須否則軟件觸發(fā) sConfig.DAC_OutputBuffer DAC_OUTPUTBUFFER_ENABLE; // 降低功耗 if (HAL_DAC_ConfigChannel(hdac, sConfig, DAC_CHANNEL_1) ! HAL_OK) { Error_Handler(); } // MX_TIM2_Init() 中關(guān)鍵設(shè)置 htim2.Instance TIM2; htim2.Init.Prescaler 35; // 36MHz / (351) 1MHz htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 99; // 1MHz / (991) 10kHz 更新頻率 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; if (HAL_TIM_Base_Init(htim2) ! HAL_OK) { Error_Handler(); } // 啟動 TIM2 并使能 TRGO 輸出關(guān)鍵 HAL_TIM_Base_Start(htim2); __HAL_TIM_ENABLE_IT(htim2, TIM_IT_UPDATE); // 可選僅用于調(diào)試計數(shù) // 注意無需開啟 TIM2 中斷TRGO 由硬件自動生成邏輯說明TIM_IT_UPDATE中斷在此僅為調(diào)試用途如每 1000 次更新翻轉(zhuǎn) LEDDAC 更新完全由 TIM2 的 TRGO 信號硬件觸發(fā)與中斷服務(wù)程序無關(guān)。即使主循環(huán)卡死DAC 波形依然穩(wěn)定——這才是工業(yè)場景需要的可靠性。3.3 主循環(huán)喂數(shù)指針輪詢 雙緩沖防撕裂單純用HAL_DAC_SetValue()在 TIM2 更新中斷里寫值仍可能因中斷嵌套導(dǎo)致某次寫入失敗。最優(yōu)解是開辟兩個緩沖區(qū)buf_a[]和buf_b[]TIM2 更新中斷中只切換當(dāng)前活動緩沖區(qū)指針主循環(huán)以高優(yōu)先級持續(xù)向非活動緩沖區(qū)填入下一批數(shù)據(jù)256 點切換瞬間完成無臨界區(qū)競爭。實際工程中我們簡化為單緩沖 指針原子切換F103 無 cachevolatile指針賦值是原子的volatile uint16_t *current_table sine_table; // 初始指向查表 volatile uint8_t table_index 0; // TIM2 更新中斷回調(diào)HAL_TIM_PeriodElapsedCallback void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { // 直接觸發(fā) DAC 更新硬件已配置此處僅推進(jìn)索引 HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, current_table[table_index]); if (table_index 256) table_index 0; } }參數(shù)說明DAC_ALIGN_12B_R表示右對齊 12 位數(shù)據(jù)低 4 位忽略sine_table是uint16_t數(shù)組每個值已按 0x000–0xFFF 范圍預(yù)計算。table_index用volatile修飾確保編譯器不優(yōu)化掉其讀寫。4. DMA 方式輸出正弦波吞吐翻倍但必須禁用雙緩沖4.1 為什么 DMA 是高采樣率剛需CPU 卸載的本質(zhì)當(dāng)目標(biāo)波形升至 50kHz20μs/點靠中斷喂數(shù)已逼近極限中斷響應(yīng) 寄存器寫入 ≈ 1.2μs256 點需 307μs占空比超 15%CPU 無暇處理其他任務(wù)更高頻率下中斷頻繁系統(tǒng)抖動加劇。DMA 方案讓 DAC 數(shù)據(jù)傳輸完全脫離 CPU配置 DMA 通道DMA1 Channel3將sine_table數(shù)組按ARR1周期循環(huán)搬運至 DAC 的 DHR1 寄存器CPU 僅需啟動一次 DMA后續(xù)全自動。4.2 CubeMX 中 DMA 配置的四個致命參數(shù)CubeMX 默認(rèn)配置極易失敗必須手動修正參數(shù)錯誤值正確值原因DMA RequestDAC_CH1DAC_CH1? 正確但需確認(rèn)勾選Data WidthByteHalf WordDAC_DHR1 是 16 位寄存器字節(jié)傳輸會錯位Circular ModeDisabledEnabled必須循環(huán)否則播完 256 點即停Double Buffer ModeEnabledDisabled?? F103 DAC 不支持雙緩沖啟用則 DMA 停止響應(yīng)提示CubeMX 生成的MX_DMA_Init()中hdma_dac_ch1.Init.MemDataAlignment必須為DMA_MDATAALIGN_HALFWORDhdma_dac_ch1.Init.PeriphDataAlignment為DMA_PDATAALIGN_HALFWORD。若為BYTE示波器將看到階梯狀畸變波形。4.3 啟動 DMA 的精確時機先啟 DAC再啟 DMA順序錯誤會導(dǎo)致 DMA 傳輸被忽略HAL_DAC_Start()啟動 DAC 通道HAL_DAC_Start_DMA()啟動 DMA注意HAL_DAC_Start_DMA()內(nèi)部會自動使能 DAC 觸發(fā)但必須確保 DAC 已處于就緒狀態(tài)HAL_TIM_Base_Start()啟動 TIM2提供觸發(fā)源。// 正確啟動順序在 main() 中 HAL_DAC_Start(hdac, DAC_CHANNEL_1); // 先讓 DAC 就緒 HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t)sine_table, 256, // 緩沖區(qū)地址與長度 DAC_ALIGN_12B_R, DMA_NORMAL); // 注意F103 不支持 CIRCULAR 模式下的 DMA_NORMAL實測可用 HAL_TIM_Base_Start(htim2); // 最后啟動定時器TRGO 開始觸發(fā)邏輯說明HAL_DAC_Start_DMA()的第 6 個參數(shù)DMA_NORMAL在 F103 上實測穩(wěn)定盡管手冊建議DMA_CIRCULAR因Circular Mode已在 CubeMX 中啟用。若用DMA_CIRCULAR需額外調(diào)用HAL_DAC_Stop_DMA()才能停止而DMA_NORMAL播完即?!覀円蕾?TIM2 的持續(xù) TRGO 保持循環(huán)故NORMAL更可控。5. 避坑五條血淚經(jīng)驗每一條都來自真實示波器抓圖5.1 現(xiàn)象DAC 輸出恒為 0V 或 3.3V原因PA4/PA5 引腳未配置為GPIO_MODE_ANALOG或 DAC 電源未使能VREF 未接 3.3V。解決用萬用表量 PA4 對地電壓若為 0V 或 3.3V立即檢查 GPIO 初始化代碼確認(rèn)開發(fā)板 VREF 引腳已焊接或跳線接入 3.3V。5.2 現(xiàn)象波形頻率正確但嚴(yán)重失真頂部削波/底部抬升原因查表公式未歸一化或sine_table數(shù)組類型為int16_t有符號導(dǎo)致高位補 1。解決用調(diào)試器查看sine_table[0]值必須為0x000最小和0xFFF最大確保數(shù)組聲明為const uint16_t sine_table[256]。5.3 現(xiàn)象使用 DMA 后波形斷續(xù)每隔 256 點出現(xiàn) 10μs 空白原因TIM2 的ARR值與查表長度不匹配。例如ARR9910kHz但表長 512則 DMA 每 512×100μs 51.2ms 才循環(huán)一次中間無數(shù)據(jù)。解決ARR必須滿足采樣率 1000000/(ARR1)且查表長度必須整除ARR1的倒數(shù)周期。簡單法固定表長 256ARR (1000000/采樣率) - 1。5.4 現(xiàn)象HAL_DAC_Start() 返回 HAL_ERROR原因DAC 時鐘未使能RCC-APB1ENR 的 DACEN 位未置 1或hdac.State非 RESET 狀態(tài)多次調(diào)用未重置。解決在MX_DAC_Init()前添加__HAL_RCC_DAC_CLK_ENABLE()調(diào)用前加hdac.State HAL_DAC_STATE_RESET;。5.5 現(xiàn)象用 Keil 下載后首次上電無波形復(fù)位后才出現(xiàn)原因DAC 輸出緩沖未使能或上電時 DAC 未完成內(nèi)部校準(zhǔn)F103 無自動校準(zhǔn)需手動觸發(fā)。解決在HAL_DAC_Init()后添加HAL_DAC_SetOutputBuffer(hdac, DAC_CHANNEL_1, ENABLE)若仍無效在main()開頭插入HAL_Delay(10)給 DAC 內(nèi)部電路穩(wěn)定時間。6. 進(jìn)階技巧用示波器驗證 DAC 性能與動態(tài)范圍壓縮6.1 用 FFT 功能看諧波失真識別 DAC 量化噪聲與定時器抖動將示波器探頭接 PA4打開 FFT 分析Span 設(shè)為 0–100kHz理想正弦波基頻如 1kHz峰值最高其余諧波2kHz, 3kHz...低于 -60dB若 2kHz 處出現(xiàn)尖峰說明 TIM2 觸發(fā)存在偶次抖動檢查 PSC/ARR 是否為整數(shù)分頻若寬帶噪聲抬高可能是電源紋波耦合嘗試在 PA4 與地之間加 100nF 陶瓷電容濾波。提示F103 DAC 的理論信噪比SNR約 72dB12 位實測通常 65–68dB。若 FFT 顯示 SNR 50dB優(yōu)先排查 PCB 地線分割和模擬電源去耦。6.2 動態(tài)范圍壓縮用查表實現(xiàn) 8 位精度輸出節(jié)省 50% Flash當(dāng)應(yīng)用只需語音提示音非 Hi-Fi可將 256 點表壓縮為 128 點并用uint8_t存儲// 壓縮版 sine_table_128.h節(jié)省 256 字節(jié) Flash const uint8_t sine_table_128[128] { 0x80, 0x86, 0x8C, 0x92, 0x98, 0x9E, 0xA4, 0xAA, // ... 公式uint8_t val (uint8_t)((sine_table[i] 4) 0xFF); };在HAL_DAC_SetValue()中擴展value ((uint16_t)table_128[idx]) 4;實測波形肉眼無差異Flash 占用從 512 字節(jié)降至 256 字節(jié)。6.3 多頻點切換用指針數(shù)組實現(xiàn) 3 種波形一鍵切換定義波形指針表通過按鍵改變current_table指向const uint16_t *wave_tables[3] {sine_table, square_table, triangle_table}; uint8_t wave_sel 0; // 0sine, 1square, 2triangle // 按鍵中斷中 if (key_pressed) { wave_sel (wave_sel 1) % 3; current_table wave_tables[wave_sel]; }square_table和triangle_table可用同樣 Python 腳本生成僅修改數(shù)學(xué)公式。從那以后我每次調(diào)試 DAC第一件事就是拿萬用表量 PA4 對地電壓——0V 或 3.3V 立刻停手查 GPIO 模式第二件事是打開示波器 FFT不看波形看噪聲底第三件事是確認(rèn)sine_table[0]和sine_table[64]的值是否精準(zhǔn)落在 0x000 和 0xFFF。這三步做完90% 的“DAC 不出波”問題當(dāng)場消失。希望幫到你。本文還有配套的精品資源點擊獲取