DAC如何用定時(shí)器+DMA 2D輸出高保真任意波形)
接到一個(gè)儀表類項(xiàng)目要在 LAT1189 上輸出幾種不同波形正弦、三角、帶可調(diào)死區(qū)的脈沖頻率和幅度都得能實(shí)時(shí)改。板子上沒(méi)有 DAC就一個(gè)定時(shí)器加幾個(gè) DMA 通道。我一開(kāi)始覺(jué)得在定時(shí)器中斷里改比較寄存器也能應(yīng)付后來(lái)把頻率拉到幾百 kHz 才發(fā)現(xiàn)根本不行CPU 占用爆表波形還抖得沒(méi)法看。后來(lái)把 DMA 的 2D 通道用起來(lái)用定時(shí)器更新事件去觸發(fā) DMA 搬運(yùn)波形表數(shù)據(jù)CPU 直接解放效果比預(yù)想好很多。這篇筆記就把這套方案的原理、參數(shù)計(jì)算、初始化和踩坑過(guò)程完整記下來(lái)給準(zhǔn)備在類似 MCU 上做波形輸出的朋友一個(gè)可復(fù)用的參考。1. 需求拆解與方案選型1.1 這個(gè)項(xiàng)目到底要輸出什么波形很多場(chǎng)景都需要 MCU 產(chǎn)生模擬信號(hào)比如信號(hào)發(fā)生器、電機(jī)驅(qū)動(dòng)、數(shù)字電源、傳感器激勵(lì)、音頻合成甚至是壓電陶瓷驅(qū)動(dòng)。這個(gè)項(xiàng)目的核心需求就一句話在指定的 GPIO 引腳上輸出頻率和形狀可變的周期性波形同時(shí)盡量不占用 CPU。具體到波形類型常見(jiàn)有這幾類方波/帶死區(qū)的互補(bǔ) PWM這是電機(jī)控制和電源最常用的三角波/鋸齒波常用于掃描信號(hào)、PWM 斜坡補(bǔ)償正弦波用于逆變、音頻及各種測(cè)試設(shè)備任意自定義波形比如從 ADC 采回來(lái)的數(shù)據(jù)回放、特殊激勵(lì)信號(hào)項(xiàng)目原計(jì)劃是用外部 DAC 芯片輸出但硬件評(píng)審時(shí)把 DAC 去掉了板子上只有一個(gè)定時(shí)器、幾個(gè) DMA 通道以及一個(gè)普通 GPIO。所以我必須用現(xiàn)有資源把“模擬波形”變出來(lái)而且性能不能太差。1.2 為什么是 Timer DMA而不是中斷或純軟件模擬剛接到任務(wù)時(shí)我第一反應(yīng)是開(kāi)一個(gè)定時(shí)器中斷每次中斷把比較寄存器的值改一下。這樣做對(duì)小頻率沒(méi)問(wèn)題但有一個(gè)致命傷中斷響應(yīng)有延遲尤其是系統(tǒng)里還有其他中斷優(yōu)先級(jí)更高時(shí)CCR 更新時(shí)機(jī)不穩(wěn)定輸出波形的周期會(huì)出現(xiàn)抖動(dòng)。高頻下更明顯比如 100 kHz 的更新率意味著 10 us 就要改一次比較值中斷里還要做查表、賦值、清標(biāo)志CPU 壓力非常大。純軟件翻轉(zhuǎn) GPIO 就更不用說(shuō)了雖然低頻能出波形但頻率一高GPIO 翻轉(zhuǎn)指令之間的延遲會(huì)讓波形完全變形而且 CPU 什么都不能干。那為什么選 Timer DMA因?yàn)槎〞r(shí)器本身能產(chǎn)生 PWM 輸出而 DMA 可以在硬件層面把保存在內(nèi)存里的波形采樣點(diǎn)數(shù)據(jù)自動(dòng)搬運(yùn)到定時(shí)器的比較寄存器。整個(gè)數(shù)據(jù)搬運(yùn)過(guò)程不需要 CPU 介入DMA 控制器自己完成而且觸發(fā)源是定時(shí)器更新事件時(shí)序非常精確不會(huì)像中斷一樣有抖動(dòng)。DMA 2D 通道則比普通 DMA 更進(jìn)一步。普通 DMA 只能按線性地址搬一長(zhǎng)串連續(xù)數(shù)據(jù)2D 模式可以理解成“按行按列搬數(shù)據(jù)”每次搬完一行數(shù)據(jù)后源地址可以按設(shè)定好的偏移跳到下一個(gè)數(shù)據(jù)塊而不是只能順序讀取。這對(duì)波形表的組織、切換、拼接都提供了額外自由度。1.3 這套方案需要的最少硬件資源如果你也打算在類似項(xiàng)目里復(fù)現(xiàn)這套方案需要確認(rèn)一下手上芯片的資源配置一個(gè)能輸出 PWM、并且能產(chǎn)生 DMA 請(qǐng)求的定時(shí)器一個(gè)支持 2D或“二維”傳輸模式的 DMA 控制器至少一個(gè)帶復(fù)用功能映射的 GPIO 引腳可選的一級(jí) RC 低通濾波用來(lái)把 PWM 載波濾掉看平滑波形LAT1189 的定時(shí)器和 DMA 部分是支持這種組合的這也是我最終選擇這套方案的原因之一。如果你的芯片沒(méi)有 2D DMA也可以先用普通 DMA 加循環(huán)模式跑通基礎(chǔ)波形后面再根據(jù)手冊(cè)把 2D 特性補(bǔ)齊。2. 核心原理PWM 如何變成任意波形2.1 PWM 調(diào)制的本質(zhì)大家通常對(duì) PWM 的印象是“調(diào)占空比控制電機(jī)速度”但從信號(hào)角度看PWM 其實(shí)是一個(gè)可以通過(guò)低通濾波還原成模擬電壓的“開(kāi)關(guān)信號(hào)”。假設(shè) PWM 頻率為 100 kHz占空比為 50%經(jīng)過(guò) RC 低通之后輸出電壓平均值就是電源電壓的一半。占空比從 0% 到 100% 變化輸出電壓平均值也跟著線性變化。所以只要讓占空比按目標(biāo)波形的采樣點(diǎn)序列連續(xù)變化輸出端濾波后的電壓就是目標(biāo)波形的復(fù)現(xiàn)。這就是用 PWM 實(shí)現(xiàn)任意波形的基本原理本質(zhì)上是“1-bit DAC”或者說(shuō)是“PWM DAC”。舉個(gè)例子要輸出一個(gè)正弦波那就在內(nèi)存里放一張表表里存的是每個(gè)采樣點(diǎn)的占空比數(shù)值。定時(shí)器每更新一次DMA 自動(dòng)把表里的下一個(gè)值寫(xiě)入比較寄存器于是每個(gè) PWM 周期的占空比都不同整體包絡(luò)就形成正弦波。2.2 數(shù)據(jù)鏈路定時(shí)器更新事件如何驅(qū)動(dòng) DMA整個(gè)數(shù)據(jù)流是這樣的定時(shí)器計(jì)數(shù)器從 0 向上計(jì)數(shù)到達(dá)自動(dòng)重載值 ARR 時(shí)溢出產(chǎn)生更新事件定時(shí)器硬件把更新事件映射為一個(gè) DMA 請(qǐng)求信號(hào)DMA 控制器收到請(qǐng)求后按照配置從內(nèi)存里的波形表讀取一個(gè)半字16 位數(shù)據(jù)該數(shù)據(jù)被寫(xiě)入定時(shí)器的比較寄存器 CCRCCR 的值決定了下個(gè) PWM 周期的占空比這里有個(gè)重要概念CCR 有預(yù)裝載功能。如果你在初始化時(shí)使能了 CCR 預(yù)裝載那么 DMA 寫(xiě)入的新值不會(huì)立刻影響當(dāng)前正在進(jìn)行的 PWM 周期而是等到下一個(gè)更新事件時(shí)才生效。這個(gè)特性保證了占空比切換是平滑的不會(huì)在周期中途突然跳變對(duì)波形質(zhì)量很重要。如果不用 DMA改成中斷里寫(xiě) CCR時(shí)序上很難保證每次都能在一個(gè)穩(wěn)定時(shí)間點(diǎn)完成更新。而 DMA 是硬件直接觸發(fā)延遲固定且極小實(shí)測(cè)下來(lái)波形周期抖動(dòng)明顯更小。2.3 DMA 2D 通道說(shuō)白了是干什么的普通 DMA 像一個(gè)只會(huì)“直線搬運(yùn)”的傳送帶你告訴它起始地址和總長(zhǎng)度它就一直往下搬。2D 模式則把搬運(yùn)任務(wù)拆成“行”和“列”兩個(gè)維度每次搬完設(shè)定長(zhǎng)度的數(shù)據(jù)后可以按你指定的步長(zhǎng)跳到新的內(nèi)存位置繼續(xù)搬。這句話拆開(kāi)理解內(nèi)層維度定義一次搬運(yùn)多少個(gè)數(shù)據(jù)比如 100 個(gè)點(diǎn)內(nèi)層步長(zhǎng)每個(gè)數(shù)據(jù)之間的地址偏移比如 16 位數(shù)據(jù)就是偏移 2 字節(jié)外層維度定義搬完一行之后下一行從哪里開(kāi)始也就是行間偏移外層行數(shù)要這樣搬幾行這正是標(biāo)題里“2D 通道”的價(jià)值所在。普通 DMA 要求波形表在內(nèi)存里是連續(xù)的2D 模式則允許你組織多個(gè)波形塊把不連續(xù)分布的數(shù)據(jù)當(dāng)成一個(gè)連續(xù)流來(lái)搬運(yùn)。實(shí)際項(xiàng)目中波形表可能放在 Flash 的不同段或者由多段數(shù)據(jù)拼接而成2D 模式可以直接搞定不需要 CPU 在中間做搬運(yùn)和拼接。2.4 參數(shù)怎么算頻率、點(diǎn)數(shù)、分辨率之間的三角關(guān)系要算參數(shù)先記住三個(gè)基本關(guān)系PWM 頻率 f_pwm 定時(shí)器時(shí)鐘 / (PSC 1) / (ARR 1)DMA 請(qǐng)求頻率 f_pwm輸出波形頻率 f_wave f_pwm / N其中 N 是每個(gè)周期的采樣點(diǎn)數(shù)示例主頻 96 MHzPSC 0ARR 959那么 f_pwm 96 MHz / 960 100 kHz。如果波形表每周期 100 個(gè)點(diǎn)輸出波形頻率就是 1 kHz。假如你想輸出 10 kHzN 仍然是 100那么 f_pwm 需要 1 MHz此時(shí) ARR 1 96即 ARR 95。這說(shuō)明高頻時(shí) PWM 計(jì)數(shù)器周期短占空比分辨率會(huì)比較低。占空比分辨率由計(jì)數(shù)器周期決定。ARR 959 時(shí)CCR 可以從 0 變到 959相當(dāng)于是 960 級(jí)分辨率接近 10 bit。ARR 95 時(shí)只有 96 級(jí)約 6.6 bit正弦波會(huì)很粗糙量化噪聲明顯。波形頻率每周期點(diǎn)數(shù)PWM頻率96MHz下(ARR1)占空比級(jí)數(shù)波形質(zhì)量1 kHz100100 kHz960960級(jí)好10 kHz1001 MHz9696級(jí)一般10 kHz50500 kHz192192級(jí)中等1 kHz200200 kHz480480級(jí)很好所以參數(shù)選擇是個(gè)三角平衡波形頻率越高、點(diǎn)數(shù)越多PWM 頻率就要越高而 ARR 越小分辨率越低。在實(shí)際項(xiàng)目里我的經(jīng)驗(yàn)是先定最高輸出頻率和可接受的最低分辨率反推需要的定時(shí)器時(shí)鐘再選擇點(diǎn)數(shù)。3. 工程落地從初始化到波形輸出3.1 前置工作引腳復(fù)用和時(shí)鐘確認(rèn)動(dòng)手寫(xiě)代碼前務(wù)必去參考手冊(cè)里查定時(shí)器的輸出通道復(fù)用關(guān)系確認(rèn)你用的 GPIO 口能映射到哪個(gè)定時(shí)器通道。不要想當(dāng)然地用任意引腳很多 GPIO 不能直接當(dāng)定時(shí)器 PWM 輸出用。時(shí)鐘部分也要確認(rèn)。很多 MCU 里APB1 預(yù)分頻如果大于 1定時(shí)器時(shí)鐘反而是 PCLK1 的 2 倍。有人配置完定時(shí)器發(fā)現(xiàn)頻率慢了一倍就是這個(gè)原因。這個(gè)坑我后面專門(mén)寫(xiě)實(shí)際踩過(guò)。3.2 定時(shí)器初始化讓更新事件成為 DMA 請(qǐng)求源定時(shí)器配置的核心目標(biāo)是輸出 PWM并且把更新事件作為 DMA 請(qǐng)求源。參考代碼如下以 LAT1189 的 SDK 風(fēng)格簡(jiǎn)化具體寄存器名參考芯片手冊(cè)調(diào)整TIM_TimeBaseInitTypeDef tim_base; TIM_OCInitTypeDef tim_oc; tim_base.Period 959; // ARR 1 960100 kHz 更新率 tim_base.Prescaler 0; // 不做額外分頻 tim_base.CounterMode TIM_COUNTERMODE_UP; tim_base.ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM0, tim_base); tim_oc.OCMode TIM_OCMODE_PWM1; // PWM 模式1CNT CCR 時(shí)輸出高 tim_oc.Pulse 0; // 初始占空比設(shè)為 0 tim_oc.OCPolarity TIM_OCPOLARITY_HIGH; tim_oc.OCFast TIM_OCFAST_DISABLE; TIM_OC1Init(TIM0, tim_oc); TIM_OC1PreloadConfig(TIM0, TIM_OCPreload_ENABLE); // 關(guān)鍵讓定時(shí)器更新事件作為 DMA 請(qǐng)求源 TIM_DMARequestEnable(TIM0, TIM_DMA_Update); TIM_Cmd(TIM0, ENABLE);注意TIM_OC1PreloadConfig這行我建議一直開(kāi)啟。原因前面說(shuō)了預(yù)裝載可以讓 CCR 在更新事件時(shí)同步生效避免占空比更新到一半出現(xiàn)毛刺。如果關(guān)掉預(yù)裝載修改 CCR 后立刻生效反而容易造成波形畸變。3.3 DMA 2D 配置源地址的二維描述這是整套方案的核心。先理清我們要搬什么源地址波形表數(shù)組的首地址目標(biāo)地址TIM0 的 CCR1 寄存器地址方向內(nèi)存到外設(shè)單次數(shù)據(jù)寬度半字因?yàn)?CCR 和波形表數(shù)值都是 16 位內(nèi)存地址遞增每個(gè)點(diǎn)遞增 2 字節(jié)外設(shè)地址不遞增每次都寫(xiě)同一個(gè) CCR1再配置 2D 模式的三個(gè)關(guān)鍵參數(shù)每行點(diǎn)數(shù)就是波形表一周期長(zhǎng)度比如 100行間偏移搬完一行后源地址跳多少字節(jié)一般設(shè)置為 0表示連續(xù)讀下一張表總行數(shù)如果要連續(xù)輸出多個(gè)周期可以設(shè)為循環(huán)次數(shù)如果只想周期性無(wú)限循環(huán)配合 DMA 循環(huán)模式即可代碼結(jié)構(gòu)大致如下DMA_2DInitTypeDef dma_2d; dma_2d.PeriphAddr (uint32_t)TIM0-CCR1; dma_2d.MemoryAddr (uint32_t)wave_sin_table; dma_2d.Direction DMA_MEMORY_TO_PERIPH; dma_2d.PeriphInc DMA_PINC_DISABLE; dma_2d.MemoryDataSize DMA_MDATAALIGN_HALFWORD; dma_2d.PeriphDataSize DMA_PDATAALIGN_HALFWORD; dma_2d.LineLength WAVE_POINTS; // 100 dma_2d.LineOffset 0; // 行間偏移單位是字節(jié) dma_2d.LineCount 0; // 0 表示無(wú)限循環(huán)配合 DMA 循環(huán)模式 dma_2d.CircularMode ENABLE; // 開(kāi)啟 DMA 循環(huán) dma_2d.ContinuousRequests ENABLE; // 開(kāi)啟連續(xù)請(qǐng)求 dma_2d.MemoryInc DMA_MINC_ENABLE; DMA_2DInit(DMA0, DMA_CH0, dma_2d); DMA_Cmd(DMA0, DMA_CH0, ENABLE);ContinuousRequests這個(gè)位非常重要。有些芯片里叫連續(xù)請(qǐng)求有些叫“循環(huán)模式”它決定 DMA 在搬運(yùn)完設(shè)定長(zhǎng)度后是否自動(dòng)重載并繼續(xù)下一次搬運(yùn)。如果這里沒(méi)有開(kāi)啟你只能看到非常短暫的單個(gè)突發(fā)波形而不是連續(xù)波形。關(guān)于LineCount不同芯片的定義可能不同。有的芯片支持無(wú)限循環(huán)有的需要設(shè)一個(gè)較大的數(shù)。如果你發(fā)現(xiàn)循環(huán)模式已經(jīng)開(kāi)了但波形仍然會(huì)停多半是外層行數(shù)配置不對(duì)仔細(xì)查一下手冊(cè)。3.4 波形表怎么生成存哪里波形表本質(zhì)是“占空比數(shù)值序列”。以 100 點(diǎn)正弦表為例我們希望每個(gè)點(diǎn)代表一個(gè) PWM 周期內(nèi)的 CCR 值CCR 值等于該時(shí)刻的占空比乘以 ARR。我在實(shí)際項(xiàng)目里用 Python 離線生成避免在工程里寫(xiě)一大段啟動(dòng)計(jì)算代碼。生成代碼import math ARR 959 N 100 for i in range(N): # 占空比在 5% ~ 95% 之間變化留出一點(diǎn)余量 duty 0.5 0.45 * math.sin(2 * math.pi * i / N) ccr int(duty * ARR) print(ccr, end, )輸出會(huì)是一串 0~959 之間的整數(shù)直接貼進(jìn) C 代碼const uint16_t wave_sin_table[100] { 479, 506, 533, 560, ... };為什么一定要用const因?yàn)槲覀冎恍枰?DMA 讀取不需要改放 Flash 能省掉寶貴的 RAM。這也體現(xiàn)了一個(gè)關(guān)鍵點(diǎn)源地址指向 Flash 是完全可以的DMA 的源數(shù)據(jù)寬度和 Flash 讀取速度要匹配。實(shí)測(cè)中 Flash 零等待狀態(tài)下沒(méi)問(wèn)題如果芯片有 Flash 等待周期高頻 DMA 搬運(yùn)時(shí)可能會(huì)有性能問(wèn)題那時(shí)可以把表放到 RAM。三角波表也類似N 100 for i in range(N): if i N // 2: duty 0.05 0.9 * i / (N // 2) else: duty 0.95 - 0.9 * (i - N // 2) / (N // 2) ccr int(duty * ARR)3.5 啟動(dòng)順序和實(shí)時(shí)切換波形啟動(dòng)順序有個(gè)細(xì)節(jié)先使能 DMA再使能定時(shí)器。如果反過(guò)來(lái)定時(shí)器已經(jīng)開(kāi)始產(chǎn)生更新事件而 DMA 還沒(méi)有準(zhǔn)備好前幾次請(qǐng)求會(huì)落空輸出的第一個(gè)周期可能是錯(cuò)誤的。DMA_Cmd(DMA0, DMA_CH0, ENABLE); // 先 DMA TIM_Cmd(TIM0, ENABLE); // 后 Timer波形切換時(shí)建議在 DMA 搬運(yùn)完成中斷里改源地址。假設(shè)你準(zhǔn)備了正弦表、三角表、方波表三張表切換過(guò)程是開(kāi)啟 DMA 傳輸完成中斷在中斷回調(diào)里把MemoryAddr改成新表地址清標(biāo)志等下一個(gè)周期自動(dòng)生效為什么一定要在中斷里改因?yàn)?DMA 正在按當(dāng)前的源地址跑如果任意時(shí)刻強(qiáng)行改源地址可能會(huì)讓當(dāng)前周期中間突然跳到另一個(gè)地址造成波形截?cái)嗷蛎獭T凇罢袀鬏斖瓿伞钡倪吔绺牡刂穭t能保證這一幀數(shù)據(jù)完整輸出下一幀才開(kāi)始切換。如果你不想頻繁打斷核心邏輯可以用 DMA 半傳輸和全傳輸雙緩沖技巧CPU 在半傳輸中斷里更新前半段表數(shù)據(jù)在全傳輸中斷里更新后半段這樣搬運(yùn)過(guò)程完全連續(xù)。這個(gè)技巧在多段拼接波形時(shí)特別好用。4. 調(diào)試實(shí)錄與常見(jiàn)問(wèn)題4.1 最常見(jiàn)問(wèn)題速查表現(xiàn)象可能原因排查與解決引腳一直高或一直低DMA 沒(méi)觸發(fā)或波形表值全是 0/全是 ARR檢查定時(shí)器 DMA 請(qǐng)求使能、DMA 通道請(qǐng)求源、波形表數(shù)值范圍只輸出一兩個(gè)周期就停DMA 循環(huán)模式/連續(xù)請(qǐng)求沒(méi)開(kāi)設(shè)置 ContinuousRequests ENABLE檢查外層行數(shù)波形頻率是預(yù)期的一半定時(shí)器時(shí)鐘源倍數(shù)搞錯(cuò)了查 RCC 時(shí)鐘樹(shù)確認(rèn) APB 預(yù)分頻對(duì) TIM 時(shí)鐘的影響占空比跳變有毛刺CCR 預(yù)裝載未開(kāi)啟或 DMA 優(yōu)先級(jí)太低開(kāi)啟 CCR 預(yù)裝載提高 DMA 通道優(yōu)先級(jí)換波形不生效修改源地址時(shí)機(jī)不對(duì)在 DMA 傳輸完成中斷里切換不要在搬運(yùn)中途改高頻時(shí)波形量化感強(qiáng)ARR 太小分辨率不足提高定時(shí)器時(shí)鐘或減少每周期點(diǎn)數(shù)運(yùn)放輸出波形差示波器探頭地線太長(zhǎng)/沒(méi)有濾波用短地線彈簧夾加一級(jí) RC 低通4.2 我調(diào)波形時(shí)遇到的兩個(gè)關(guān)鍵時(shí)刻第一個(gè)問(wèn)題是只輸出一個(gè)脈沖再接一個(gè)該死的高電平。排查時(shí)鐘、引腳、DMA 都比較正常后來(lái)發(fā)現(xiàn)是 DMA 循環(huán)模式?jīng)]開(kāi)。定時(shí)器更新事件確實(shí)觸發(fā)了 DMADMA 搬完 100 個(gè)點(diǎn)后認(rèn)為任務(wù)完成就停在那里而定時(shí)器還在繼續(xù)輸出CCR 停留在最后一個(gè)值上所以引腳就變成了固定電平。解決辦法就是開(kāi)循環(huán)模式讓 DMA 永遠(yuǎn)循環(huán)讀波形表。第二個(gè)問(wèn)題是頻率慢了一倍。我按照 PCLK1 的 48 MHz 計(jì)算定時(shí)器怎么算都不對(duì)實(shí)際輸出頻率正好是預(yù)期的一半。后來(lái)翻了芯片參考手冊(cè)的時(shí)鐘樹(shù)才發(fā)現(xiàn) APB1 預(yù)分頻系數(shù)不為 1 時(shí)定時(shí)器時(shí)鐘是 APB1 的兩倍也就是 96 MHz而我還拿 48 MHz 在算。這個(gè)問(wèn)題在各種 MCU 上都很常見(jiàn)配置定時(shí)器前務(wù)必先確認(rèn) TIMx_CLK 的真實(shí)來(lái)源。4.3 測(cè)量波形別用錯(cuò)誤方法騙自己測(cè)量 PWM 調(diào)制的模擬波形有兩個(gè)地方特別容易踩坑。首先是示波器接地。探頭的地線夾如果太長(zhǎng)等效于一個(gè)天線會(huì)引入很多高頻噪聲毛刺看起來(lái)特別多。建議用探頭自帶的短接地彈簧直接夾在靠近測(cè)試點(diǎn)的地線上。其次是濾波問(wèn)題。直接用示波器看 PWM 引腳你看到的是一堆高頻脈沖而不是平滑正弦波。需要一級(jí) RC 低通把載波濾掉。常見(jiàn)取值 R 1 kΩC 10 nF截止頻率大約 16 kHz對(duì) 1 kHz 正弦波影響很小但對(duì) 100 kHz 的 PWM 載波衰減比較明顯。如果驅(qū)動(dòng)負(fù)載建議 RC 后加一級(jí)運(yùn)放電壓跟隨器否則負(fù)載電阻會(huì)改變 RC 濾波特性。5. 擴(kuò)展思路這套 DMA 2D 還能怎么用5.1 不止是波形輸出還能做 ADC 多通道數(shù)據(jù)重組這套 Timer DMA 2D 思維可以用在很多地方。最典型的是 ADC 多通道掃描循環(huán)采樣。普通 DMA 做 ADC 采樣時(shí)數(shù)據(jù)到達(dá)的順序按 ADC 通道順序排列如果你對(duì)不同通道的數(shù)據(jù)做不同處理需要在內(nèi)存里排序會(huì)有 CPU 開(kāi)銷。用 2D DMA 可以配置地址步進(jìn)讓每一通道的數(shù)據(jù)直接落到獨(dú)立緩沖區(qū)CPU 只管處理。這個(gè)思路在很多需要連續(xù)采集的項(xiàng)目里很實(shí)用。串口 DMA 接收也是一樣的套路。配合空閑中斷可以把不定長(zhǎng)數(shù)據(jù)包完整接收進(jìn)環(huán)形緩沖區(qū)避免主循環(huán)頻繁進(jìn)出串口中斷。DMA 2D 地址跳轉(zhuǎn)能力還能把多個(gè)不同緩沖區(qū)的數(shù)據(jù)拼接成一條報(bào)文直接發(fā)送省掉內(nèi)存拷貝。5.2 如果芯片不支持 2D DMA怎么降級(jí)如果你換了一顆沒(méi)有 2D 模式的 MCU不要慌方案依然能實(shí)現(xiàn)只是波形表必須是連續(xù)內(nèi)存塊。降級(jí)思路是用普通 DMA 通道加循環(huán)模式一張表一個(gè)任務(wù)切換波形時(shí)在 DMA 傳輸完成中斷里修改內(nèi)存起始地址如果同時(shí)要輸出多路不同波形每路 PWM 通道占用一個(gè)普通 DMA 通道這樣做的代價(jià)是波形表組織不靈活但基本功能仍然可用。2D 通道更像是一個(gè)優(yōu)化選項(xiàng)讓表與表之間的編排更自由。我個(gè)人的體會(huì)是這種“硬件自動(dòng)搬運(yùn)數(shù)據(jù)到外設(shè)”的思路能解決很多 CPU 忙不過(guò)來(lái)的實(shí)時(shí)性問(wèn)題。做嵌入式越多越會(huì)發(fā)現(xiàn) Timer、DMA 這些外設(shè)的潛力遠(yuǎn)比想象中大關(guān)鍵是建模清楚“什么事件觸發(fā)什么搬運(yùn)”這條鏈路。遇到類似波形輸出需求先不要急著寫(xiě)中斷翻翻手冊(cè)看看硬件能不能自己完成往往會(huì)有意外驚喜。