
简介面向STM32F103开发者这份资源提供了国产DAC芯片SGM5348-12的应用驱动重点解决网上缺少该芯片可参考驱动代码的问题。SGM5348拥有8路独立模拟输出输出范围0-5V10mA体积小巧、应用广泛可广泛用于需要多路独立模拟电压输出的自动化设备、测试仪器、DCS系统等场景。压缩包内共包含2个文件分别是1个C驱动源文件与1个头文件整体大小仅3KB代码精简结构清晰不仅易于阅读和直接嵌入工程也可作为SPI设备驱动的移植参考。驱动基于SPI接口实现已封装初始化、通道切换和输出电压设置等关键逻辑配合数据手册使用可快速掌握芯片操作流程省去自行阅读寄存器时序的繁琐工作显著提升开发效率也降低了新手入门门槛。目前已有2489人学习下载适合正在选型国产DAC或急需在STM32上实现多路模拟输出的开发者对于学习DAC驱动编写也有参考价值。 前阵子做一个多路模拟量输出模块主控是STM32F103C8T6需求倒不复杂8路0~2.5V可调电压上位机能随时改任意一路输出精度要求12位。一开始想直接用F103内部DAC结果一看手册就放弃了——内置DAC只有2路还没有输出缓冲带不动负载想凑8路还得外挂模拟开关方案又丑又不稳定。换成圣邦微SGM5348-12这颗国产DAC之后8通道12位、SPI接口、3.3V单电源一片就解决了所有通道需求。这篇文章把SGM5348-12在STM32F103上的驱动开发过程完整记录下来包括选型原因、硬件注意点、SPIDMA驱动代码以及我在调试中踩过的坑给同样要在F103上扩展多路DAC的朋友一份可以直接抄的作业。1. 硬件选型与整体方案拆解1.1 为什么不用芯片内置DACSTM32F103系列大部分型号内置2路12位DAC很多刚接触模拟量输出的人第一反应就是“直接用它”。实际做项目时你会很快发现问题一是通道数量不够两路输出在工业现场基本只够当个调试口二是内置DAC输出阻抗高手册明说需要外部运放缓冲否则接个稍微大点的负载电压就掉下去三是它挂在APB1总线上如果系统里还要跑ADC、定时器、串口DAC的实时性和稳定性会被其他总线占用影响。也有人说用PWMDMA来做多路模拟量通过定时器输出比较加RC低通滤波得到直流电压。这个方案我在早期项目里试过做LED调光、蜂鸣器控制没问题但做12位精度的电压输出完全是另一个量级PWM的有效分辨率受限于定时器位宽滤波后纹波又和负载电流、滤波电容直接相关温度一漂输出就跟着飘标定起来非常头疼。所以多路高精度模拟量输出老老实实上外部DAC芯片才是正道。外部DAC的好处很直接通道数、分辨率、接口形式都能按需求选输出端内部集成缓冲放大器驱动能力有保障。SGM5348-12就是在这种背景下选中了。8通道、12位、SPI接口、3.3V单电源运行、轨到轨输出一个片子正好覆盖8路需求PCB面积也不大。1.2 读懂SGM5348-12的关键参数SGM5348-12是圣邦微电子推出的12位8通道电压输出型DAC采用SPI接口控制输出范围是0V到VREF。这类串行DAC内部通常是R-2R梯形电阻网络加输出缓冲放大器数字侧只负责把代码转换成对应的模拟电压。芯片的VREF引脚决定了满量程输出所以这颗DAC能做到多高的精度一半由参考电压决定。12位分辨率对应的最小输出电压步进也就是LSB直接关系到你能不能把目标电压调准。计算公式是VOUT VREF × CODE / 4096 LSB VREF / 4096如果VREF用2.5V基准一个LSB约等于0.61mV如果直接用3.3V供电电压当参考一个LSB约等于0.81mV。从设计精度角度我建议不要用VDD当参考因为单片机供电本身有纹波和温漂会让输出跟着飘。最好外接一个低温漂基准源比如REF3025、ADR4525这类把VREF固定在2.5V或3.0V。数字接口方面STM32F103的GPIO输出高电平是3.3VSGM5348-12在3.3V供电时输入高电平阈值大约是0.7倍VDD2.31V左右直接相连完全满足要求不需要额外电平转换。这点比一些5V接口的DAC省事不用加MOS管或者电平转换芯片。1.3 驱动方案设计思路驱动方案我最终定的是SPI1主模式、片选用普通GPIO控制。SPI1挂在APB2总线上最高可以跑到18MHz对于单次16bit数据帧来说几百k的更新率完全够用。GPIO软件控制CS而不是硬件NSS是因为F103的硬件NSS在这种多通道DAC场景下用起来别扭CS的拉低和拉高时机想做到精确可控还是GPIO最直接。普通单通道写入用阻塞式SPI发送就够了简单可靠。但多通道同时刷新时如果一帧一帧地发送每帧之间CS都要拉高再拉低输出电压会出现明显的逐个跳变现象这在某些执行机构同步控制的场景是不能接受的。所以批量更新用SPIDMA完成先准备好8个通道的数据帧CS一直保持低电平让8帧数据连续从MOSI发出去全部发完后再由DMA传输完成中断把CS拉高DAC一次性锁存全部通道的输出实现多通道同步更新。这样设计还有一个好处CPU不需要盯着SPI寄存器等每一个字节DMA搬运16bit数据本身就是半字操作效率比中断方式高很多。更新8个通道实际CPU只需要做一次触发和一次回调处理剩下的时间可以继续跑控制逻辑。这也是标题里提到的“DMA句柄与DAC句柄关联”的意义所在SPI外设和DMA通道通过HAL库的__HAL_LINKDMA绑定后续调用HAL_SPI_Transmit_DMA就能自动走DMA通道。2. 硬件连接与参数细节2.1 引脚分配与接线我用的主控是STM32F103C8T6SPI1引脚分配如下。如果你拿到手的板子引脚不同换掉引脚宏定义即可驱动逻辑不用动。SGM5348-12引脚STM32F103引脚说明SCLKPA5SPI1_SCK时钟输入DINPA7SPI1_MOSI数据输入SYNCPA4GPIO输出软件控制片选VREF外部基准源输出2.5V低温漂基准VDD3.3V电源靠近引脚放去耦电容GNDGND模拟地/数字地单点连接VOUT0~VOUT7后级运放/负载8路模拟输出接线时有一点必须注意SYNC片选引脚不能悬空。芯片在上电初始化阶段如果SYNC脚电平不定内部移位寄存器可能收到随机数据导致输出端出现不明电压。即使你在软件里很快配置了CS方向硬件上也应该在SYNC引脚加一个10kΩ上拉电阻到VDD保证上电期间CS处于高电平状态。2.2 参考电压与电容容量选择前面提到VREF不要直接用3.3V这里再单独说下电容选择因为很多人在DAC电路上栽跟头就是电容没选对。VREF引脚的电容选择直接影响参考电压的稳定性和噪声。我实际用的是10uF钽电容并联100nF陶瓷电容10uF负责低频稳压100nF滤高频干扰两者配合覆盖不同频段。DAC内部R-2R梯形网络在工作时会从VREF吸取微小电流如果基准输出阻抗偏高或者VREF电容太小不同代码对应的输出就会出现几个mV的偏差表现出来就是“有些码值准、有些码值不准”。VDD电源引脚同样需要10uF100nF去耦电容尽量靠近引脚放置走线要短。很多国产DAC的文档里都会给参考布局图照做就行。输出端的电容容量要克制。DAC内部已经集成了输出缓冲运放这个运放驱动一个阻性负载没问题但如果你在输出引脚上直接并联一个10uF电容运放很容易振荡输出波形上会出现几百kHz甚至MHz级别的振铃。正确的做法是如果需要滤波在DAC输出和负载之间串一个电阻再在负载端对地并联电容组成RC低通。我用的是100Ω串联加100nF对地截止频率大约是16kHz专门滤掉SPI数字噪声耦合过来的高频毛刺。如果输出要驱动重负载更合理的方案是外部再加一级运放跟随而不是靠加大电容。2.3 电源与地线布局模拟电路最怕数字噪声串进来。SGM5348-12虽然是一个单芯片但它内部既有数字移位寄存器又有模拟梯形网络如果电源和地处理不好数字信号很容易通过电源引脚耦合成模拟噪声输出波形上出现毛刺。我的处理方式主控和DAC的3.3V分别供电中间加磁珠隔离磁珠选600Ω100MHz的规格模拟地数字地在PCB上单点连接位置选在DAC芯片附近的GND焊盘处。另外DIN、SCLK、SYNC这三根数字线尽量远离VOUT0~VOUT7输出走线特别是VOUT走线要短、要粗避免数字信号边沿的谐波耦合到模拟输出上。3. 驱动代码实现3.1 SPI与DMA初始化代码直接基于STM32CubeMX生成的HAL工程。CubeMX里把SPI1配置成Master、16bit数据位、CPOL0、CPHA0SPI Mode 0、软件NSS波特率分频选8也就是SPI时钟约9MHz。DMA配置里添加SPI1_TXDMA1 Channel3方向MemoryToPeripheral外设地址不自增内存地址自增数据宽度半字Normal模式。最关键的一步是让DMA句柄和SPI句柄关联HAL库里用的是__HAL_LINKDMA这样之后调用HAL_SPI_Transmit_DMA时SPI外设会自动启动DMA搬运不需要手动操作DMA寄存器。SPI_HandleTypeDef hspi1; DMA_HandleTypeDef hdma_spi1_tx; void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_16BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(hspi1); } void MX_DMA_Init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); hdma_spi1_tx.Instance DMA1_Channel3; hdma_spi1_tx.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_spi1_tx.Init.PeriphInc DMA_PINC_DISABLE; hdma_spi1_tx.Init.MemInc DMA_MINC_ENABLE; hdma_spi1_tx.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_spi1_tx.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_spi1_tx.Init.Mode DMA_NORMAL; hdma_spi1_tx.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_spi1_tx); __HAL_LINKDMA(hspi1, hdmatx, hdma_spi1_tx); }CubeMX生成的初始化函数里会自动调用HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn)和HAL_NVIC_EnableIRQ(SPI1_IRQn)这两个中断必须打开DMA传输完成回调靠的就是DMA中断触发。默认初始化顺序是MX_DMA_Init在MX_SPI1_Init之前这个顺序不能反因为LINKDMA宏需要DMA句柄先创建。3.2 单通道写入函数SGM5348-12单个通道的写入帧格式是16位最高4位是通道地址低12位是DAC数据。下面的函数是驱动最底层的单通道写入任何上层操作最后都会调用它。#define DAC_CS_LOW() HAL_GPIO_WritePin(DAC_CS_GPIO_Port, DAC_CS_Pin, GPIO_PIN_RESET) #define DAC_CS_HIGH() HAL_GPIO_WritePin(DAC_CS_GPIO_Port, DAC_CS_Pin, GPIO_PIN_SET) uint8_t SGM5348_WriteChannel(uint8_t ch, uint16_t value) { uint16_t frame 0; if (ch 7) return 1; if (value 0x0FFF) return 1; frame (uint16_t)((ch 0x07) 12) | (value 0x0FFF); DAC_CS_LOW(); if (HAL_SPI_Transmit(hspi1, (uint8_t *)frame, 1, 10) ! HAL_OK) { DAC_CS_HIGH(); return 2; } DAC_CS_HIGH(); return 0; }需要注意frame是uint16_t而HAL_SPI_Transmit的pData参数是uint8_t*所以代码里做了强转。HAL库在16位数据帧模式下内部会把Size按半字数处理这里Size填1就表示发送1个16位数据。如果对端锁存的是字节流这里需要手动把高字节放前面发但我实测在16位SPI模式下直接把frame指针传进去是没问题的STM32的SPI外设会自动按MSB先行移出。片选时序是先把CS拉低然后启动数据传输传完再把CS拉高。DAC在CS的上升沿锁存数据所以这个顺序不能反。阻塞式发送每次大概耗时几微秒单通道操作不追求速度这个函数完全够用。3.3 DMA批量更新多通道批量更新才是这个驱动真正有价值的地方。前面说过多通道同步更新的关键在于整个传输期间CS保持低电平所有帧发完后CS再拉高。如果每帧之间CS都翻转一次通道输出就会按顺序逐个跳变这在同步控制场景里是致命的。#define DAC_CH_NUM 8 uint16_t dac_buf[DAC_CH_NUM]; volatile uint8_t dac_dma_busy 0; void SGM5348_WriteAllDMA(void) { while (dac_dma_busy ! 0) {} dac_dma_busy 1; DAC_CS_LOW(); if (HAL_SPI_Transmit_DMA(hspi1, (uint8_t *)dac_buf, DAC_CH_NUM) ! HAL_OK) { DAC_CS_HIGH(); dac_dma_busy 0; } } void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { DAC_CS_HIGH(); dac_dma_busy 0; } }dac_dma_busy这个变量是主程序和DMA中断之间的握手信号必须定义为volatile否则优化器可能把它优化到寄存器里主循环判断永远不生效。调用SGM5348_WriteAllDMA前先把8个通道的数据按帧格式填到dac_buf里数据格式和单通道一样高4位通道地址、低12位数据。DMA一次连续发送8个16位数据发送期间CS始终为低最后一个bit移出后DMA传输完成中断触发回调函数里拉高CSDAC同时锁存8个通道的输出。实测在9MHz SPI时钟下8通道更新一次大约20usCPU几乎不参与搬运过程。这里还有一个很容易踩的坑dac_buf数据在DMA传输期间绝对不能修改。因此上层在调用SGM5348_WriteAllDMA之后不要再立刻更新dac_buf要么等dac_dma_busy归零要么额外加一个双缓冲机制。我目前用的就是简单轮询等待实际项目里如果你不想阻塞主循环可以在回调里发信号量或者置事件标志把“更新数据”和“启动发送”拆到不同状态。3.4 电压值与寄存器码互转上位机传来的通常是电压值比如2.5V、1.234V而DAC寄存器需要的是12位整数码。转换函数很简单四舍五入处理一下就行。uint16_t SGM5348_VoltToCode(float vref, float volts) { float code; if (volts 0) volts 0; if (volts vref) volts vref; code volts * 4096.0f / vref 0.5f; return (uint16_t)code; }用/4096而不是/4095是因为SGM5348-12这类DAC的满量程输出是VREF × 4095/4096换算公式按4096算逻辑更顺最高输出码4095对应约VREF - 1LSB。这个细节不影响大碍但在标定时要心里有数。4. 调试实录与常见问题排查4.1 先看波形再查代码我第一次上板调试时程序写完烧进去测输出发现全是0V第一反应是芯片坏了或者焊接问题。后来拿示波器勾SYNC、SCLK、DIN三个引脚马上看出问题了SCK空闲电平和数据采样边沿跟DAC的要求对不上数据在DAC内部被整体移了一位出来的值自然不对。所以遇到“输出不对”这类问题第一步永远是用示波器看时序波形不要对着代码瞎改。重点看三点SCK空闲电平是高还是低数据是在SCK上升沿还是下降沿被锁存CS低电平期间完整传输了几个16位帧。SGM5348-12的通信模式要和数据手册时序图对齐我用的是CPOL0、CPHA0如果你的板子或者芯片批次要求不一样配置要跟着调。4.2 输出误差与软件校准DAC输出和理论值之间的误差主要来自两个地方一是VREF本身不精确二是DAC内部梯形网络电阻的绝对误差。如果VREF用的是普通LDO输出误差可能到2%甚至更高满量程输出自然不准。换一片高精度基准后我的实测误差大概在±3mV左右这已经是VREF精度占主导了。如果还想要更高精度可以做软件校准。方法是在出厂前给每个通道写两个点1/4量程和3/4量程用高精度万用表测实际输出电压算出增益误差和零漂把修正系数存到STM32F103内部Flash的最后一页。上电时读出系数每次写入前先做一次线性修正。实际工程中这样做基本能把误差压到±1~2mV对于绝大多数工业场景足够用了。注意Flash写入次数限制F103的Flash擦写寿命典型是10k次所以校准系数只在上电或标定流程里写运行过程中不要频繁写。另外擦写Flash时会暂停CPU执行如果恰好在DMA传输过程中触发可能导致SPI时序被拉长所以写Flash之前先确保DAC传输已经结束我的做法是调用写Flash前把dac_dma_busy等待清零。4.3 掉电保存与低功耗模式适配这个模块要做到掉电后恢复上次输出电压这就需要把DAC输出值保存到非易失存储里。我的做法是每次上位机修改输出电压后把8个通道的码值打包成结构体写入Flash的专用扇区。上电后先读Flash如果校验通过初始化完SPI和DMA之后立刻用SGM5348_WriteAllDMA恢复输出。不要在DAC初始化之前就把数据丢了先输出再校验比先校验再输出更安全避免上电瞬间DAC输出不确定电压。如果你需要做低功耗进入STOP模式前也有讲究。F103进入STOP模式后外设时钟会停止SPI引脚电平会保持但如果CS正好停在低电平DAC可能会把引脚上的随机电平误认为有效数据。我一般会在进入STOP前先把所有DAC通道设为安全电压然后调用HAL_SPI_DeInit关闭SPI最后再把SCK、MOSI、SYNC三个GPIO设为推挽输出低电平确保没有一根线处于浮空或半高电平状态。这样DAC输出保持在被设定的电压上不会乱跳。4.4 其他容易踩的坑再整理几个实际项目里高频出现的问题。关于内部晶振F103可以用内部8MHz晶振跑省两个引脚但对时序敏感的外设不友好。SPI通信本身对时钟精度容忍度较高但如果你同时开了串口和DAC偶发出现数据错位大概率是内部晶振温漂加上代码里有阻塞延时导致的。我的建议是只要PCB空间允许就上8MHz外部晶振省心很多。关于GPIO耐压问题上次有人问F103的GPIO能不能直接接5V逻辑。F103大部分IO是FT引脚可以承受5V但SGM5348-12是3.3V供电数字输入引脚不建议直接接5V虽然芯片可能没事但长期可靠性不好说。确保SCK、DIN、SYNC都是3.3V逻辑就行不需要电平转换。关于DMA传输中碰SPI不要在dac_dma_busy为1的时候去调用HAL_SPI_Transmit或者关闭SPI否则DMA和CPU同时在操作SPI外设轻则数据错乱重则卡死。我加了while (dac_dma_busy ! 0) {}就是防止这种冲突。如果用到FreeRTOS这套机制需要换成互斥锁或者信号量直接轮询等待在RTOS里会浪费CPU时间片。5. 实测效果与扩展建议5.1 实际运行表现这套驱动在两层板子上跑过SPI时钟9MHz8通道同步刷新一次约20us输出建立时间在示波器上看大概是5us以内负载是1kΩ电阻加100nF电容。用6位半万用表测了8个通道在0V、1.25V、2.5V三个点的输出和理论值最大偏差2.8mV其中一部分来自万用表精度一部分来自VREF自身的纹波。线性度表现不错从低端到高端没有发现明显的台阶性突变说明R-2R梯形网络的精度是靠谱的。我还做了连续24小时老化测试输出1.25V设定值温度在25℃左右浮动时输出变化不超过±1.5mV这已经完全满足绝大多数工业控制的需求。国产DAC芯片文档和例程确实没有进口大厂做得好很多细节要自己拿着示波器测但实际性能并不差这一颗用下来我是比较满意的。5.2 功能扩展方向这个驱动还可以往几个方向扩展。第一是增加输出级在DAC外部加一级运算放大器把0~2.5V输出范围放大到0~10V或者±10V很多PLC模拟量模块采用的就是这种架构。第二是加回读闭环用STM32的ADC采集DAC输出电压配合软件校准做闭环补偿这样即使DAC老化或者温度变化输出精度依然能维持住。第三是多片级联把多片SGM5348-12挂在同一条SPI总线上每片单独用一根CS控制驱动函数从通道号扩展成“片选号通道号”即可代码改动不大。5.3 一点个人体会这块板子做下来给我最大感触是调试DAC时一定要把注意力放在时序和参考电压这两个根上。时序不对代码写得再漂亮输出也是错的参考电压不干净精度再高的DAC也发挥不出来。画板子前先仔细读数据手册里的时序图和应用电路布局时把去耦电容放到位调试时先拿示波器确认波形再谈其他优化这条路走下来基本不会走偏。本文还有配套的精品资源点击获取