简介这份资源面向使用STM32F103进行国产DAC芯片开发的嵌入式工程师与学习者针对SGM5348-12这颗8路输出、0-5V10mA的国产DAC芯片提供了一套可直接参考的SPI应用驱动。由于网络上缺少该芯片的驱动示例作者结合实际调试经验整理出这份代码帮助读者快速完成芯片的初始化、通道配置与电压输出控制降低上手门槛。压缩包共2个文件包含1个c源文件与1个h头文件分别承载SPI通信与寄存器配置逻辑以及对外接口声明整体约3KB结构精简便于直接移植进现有工程。目前已有2498人学习下载说明该驱动在国产DAC替代方案中具有一定参考价值。配合SGM5348-12数据手册阅读代码读者可较快理解时序、命令字与通道映射关系并在此基础上扩展多路输出、电压校准等应用适合作为国产8路DAC驱动开发的入门与移植参考。1. 国产DAC芯片SGM5348-12在STM32F103上的驱动落地从选型到跑通如果你手头有一块STM32F103的开发板又恰好拿到几片SGM5348-12想把它们连起来输出一个稳定的模拟电压那这篇笔记就是为你写的。SGM5348-12是一颗12位、8通道、SPI接口的电压输出型DAC芯片国产替代场景里出现频率越来越高常见于工业控制、可编程电压源、传感器校准这类需要多通道模拟输出的场合。它和STM32F103搭配的核心逻辑并不复杂F103用SPI把12位数据写进芯片的移位寄存器芯片内部R-2R电阻网络把数字量转成电压再通过输出缓冲器驱动负载。但真正动手时你会发现时序模式选错、参考电压接法不对、通道地址算错任何一个细节都能让你在示波器前盯着一条纹波发呆。下面按“先搞清楚芯片怎么用、再动手写驱动、最后排坑”的顺序展开适合刚接触这颗芯片的嵌入式工程师也适合想从STC或Arduino平台迁移到STM32的开发者。2. SGM5348-12的通信协议与STM32F103的SPI匹配先看懂时序再接线2.1 芯片的帧格式与命令字结构SGM5348-12的SPI接口不是标准的“发完16位就完事”那种。它的每一帧是24位前8位是命令字节后16位是数据。命令字节里包含了通道选择位和功能位。具体来说命令字节的bit7~bit4是通道地址0~7对应8个通道bit3是保留位bit2是掉电控制位bit1和bit0是操作模式位。数据部分虽然是16位但实际有效的只有低12位高4位是无关位通常填0。这意味着你不能直接用STM32的SPI_DataSize_16Bit模式发一次就完事要么发三次8位要么发一次8位加一次16位或者干脆用软件模拟SPI。我一般会选硬件SPI发三次8位因为F103的SPI支持8位数据帧发三次刚好凑成24个时钟逻辑清晰中断里也好处理。这里有个容易翻车的点SGM5348-12的SPI模式是CPOL0、CPHA0也就是空闲时SCK为低数据在SCK上升沿被采样。STM32F103的SPI1配置成Master、全双工、8位、MSB First、分频系数根据你的SCK需求来定。如果你用SPI2或SPI3配置逻辑一样只是引脚不同。注意F103的SPI最高时钟是PCLK2的一半SPI1或PCLK1的一半SPI2/372MHz主频下SPI1最高36MHz但SGM5348-12的SCK最高只到30MHz所以分频系数至少设成4也就是18MHz稳妥一点用8分频到9MHz时序余量更大。2.2 硬件连接与参考电压的接法接线本身不复杂但参考电压的接法直接决定输出精度。SGM5348-12的VREF引脚接外部基准输出范围是0到VREF。如果你用3.3V供电VREF也接3.3V那输出满量程就是3.3V但3.3V轨上的噪声会直接调制到输出上。我一般会外接一颗2.5V的基准芯片比如REF5025或类似型号这样输出范围是0到2.5V噪声和温漂都小很多。VDD接3.3V注意在VDD和VREF引脚旁边各放一个0.1uF的陶瓷电容到地距离引脚越近越好。输出引脚OUT0~OUT7直接接负载但如果负载是容性的比如长电缆或某些传感器最好串一个几十欧姆的电阻再并一个小电容防止输出缓冲器振荡。SPI引脚方面STM32F103的SPI1默认是PA5SCK、PA6MISO、PA7MOSI片选CS用任意GPIO比如PA4。SGM5348-12的SDI接MOSISCK接SCKCS接GPIOSDO可以不接因为这颗芯片只写不读。如果你要读回寄存器状态那得接SDO但大多数场景下不需要。注意CS的时序SGM5348-12要求CS在24个时钟周期内保持低电平发完最后一个时钟后拉高。如果你用硬件SPI的NSS引脚自动控制可能会因为NSS脉冲宽度不够导致帧错误所以我习惯用普通GPIO手动控制CS发数据前拉低发完拉高中间不要有额外延时。2.3 用STM32CubeMX配置SPI的实操步骤打开CubeMX选好你的F103型号在Connectivity里启用SPI1Mode选Full-Duplex MasterHardware NSS Signal选Disable。Parameter Settings里Frame Format选MotorolaData Size选8 BitsFirst Bit选MSB FirstPrescaler选8Clock Polarity选LowClock Phase选1 Edge。这样配置出来的SPI就是CPOL0、CPHA0SCK频率9MHz。GPIO方面PA5和PA7自动配置为复用推挽PA6可以不管PA4手动配置为GPIO Output初始电平高。生成代码后在main.c里先调用HAL_SPI_Init然后写一个发送24位的函数。// 发送24位数据到SGM5348-12 // cmd: 命令字节包含通道和模式 // data: 12位数据右对齐 void SGM5348_Write(uint8_t cmd, uint16_t data) { uint8_t buf[3]; buf[0] cmd; buf[1] (data 8) 0x0F; // 高4位有效 buf[2] data 0xFF; // 低8位 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // CS拉低 HAL_SPI_Transmit(hspi1, buf, 3, 100); // 发3字节 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // CS拉高 }这段代码里buf[1]只取data的高4位因为SGM5348-12的12位数据在16位帧里是右对齐的高4位无效。HAL_SPI_Transmit的第四个参数是超时时间单位毫秒100足够。注意CS拉低和拉高之间不要加延时HAL_SPI_Transmit本身会阻塞到发完发完立刻拉高CS即可。如果你在中断里调用这个函数确保SPI没有其他任务在用否则要加互斥锁。3. 从零写一个可复用的SGM5348-12驱动寄存器映射与通道更新3.1 命令字节的位定义与通道地址计算命令字节的bit7~bit4是通道地址0x00对应通道00x10对应通道1以此类推0x70对应通道7。bit3保留写0。bit2是掉电控制写0是正常工作写1是掉电模式。bit1和bit0是操作模式00是写数据到输入寄存器但不更新输出01是写数据并更新输出10是写数据到输入寄存器并更新所有通道11是保留。实际使用中最常用的是01模式也就是写一个通道就立刻更新那个通道的输出。如果你要同时更新多个通道可以先用00模式把数据写到各通道的输入寄存器最后发一个命令字节0x0F通道地址0模式10来同时更新所有通道。这个特性在做多通道同步输出时很有用比如三相电压源需要同时改变三个通道的输出。通道地址的计算很简单channel 4。比如channel3地址就是0x30。命令字节 (channel 4) | (mode 0)。mode1就是写并更新。所以写通道3输出2.0V假设VREF2.5V的命令字节是0x31数据是(2.0/2.5)*4096 3277十六进制0xCCD。注意数据是12位最大值4095对应VREF所以2.0V对应3276.8取3277。3.2 用结构体封装驱动并支持多片级联如果你的系统里不止一片SGM5348-12比如需要16个通道那就得级联。SGM5348-12支持菊花链把第一片的SDO接第二片的SDICS共用SCK共用。发数据时先发第二片的24位再发第一片的24位这样第一片的数据会被推到第二片。STM32这边只需要发48位也就是6个字节。为了代码好维护我一般用一个结构体来管理每片DAC的CS引脚和当前通道值。typedef struct { GPIO_TypeDef *cs_port; uint16_t cs_pin; uint16_t channel_val[8]; // 缓存各通道当前值 } SGM5348_Handle; // 写单通道并更新 void SGM5348_SetChannel(SGM5348_Handle *h, uint8_t ch, uint16_t val) { uint8_t cmd (ch 4) | 0x01; // 写并更新 uint8_t buf[3]; buf[0] cmd; buf[1] (val 8) 0x0F; buf[2] val 0xFF; HAL_GPIO_WritePin(h-cs_port, h-cs_pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, buf, 3, 100); HAL_GPIO_WritePin(h-cs_port, h-cs_pin, GPIO_PIN_SET); h-channel_val[ch] val; }这个结构体里channel_val数组用来缓存每个通道的当前值方便做增量调整或掉电恢复。如果你要级联两片就定义两个SGM5348_HandleCS引脚不同但SPI总线共用。发数据时先发第二片的3字节再发第一片的3字节CS在两次发送之间保持低电平。注意HAL_SPI_Transmit一次发6字节也行但要把两片的buf拼在一起。3.3 电压换算与校准从数字量到实际输出电压换算公式是Vout (val / 4096) * VREF。但实际芯片有增益误差和偏移误差尤其是国产芯片批次之间可能有差异。如果你需要高精度建议做两点校准先写0测实际输出V0再写4095测实际输出VFS。然后实际写值 (目标电压 - V0) / (VFS - V0) * 4095。这个校准系数可以存在STM32的Flash里上电时读出来。我一般会在驱动里加一个校准结构体存gain和offset换算时先算理论值再套校准。typedef struct { float gain; float offset; } DAC_Calib; uint16_t DAC_VoltageToCode(float voltage, float vref, DAC_Calib *cal) { float code (voltage / vref) * 4095.0f; code code * cal-gain cal-offset; if (code 0) code 0; if (code 4095) code 4095; return (uint16_t)(code 0.5f); }这段代码里gain和offset默认是1.0和0.0校准后填入实际值。注意浮点运算在F103上没有硬件FPU但DAC更新频率不高的话软件浮点完全够用。如果你在中断里频繁调用建议改成定点运算把gain和offset放大10000倍用整数算。4. 避坑与排查SGM5348-12驱动中最容易翻车的5个点4.1 输出一直是0或者满量程现象写任何值输出要么是0V要么是VREF不随数据变化。原因通常是SPI模式不对数据没被正确移入。SGM5348-12要求CPOL0、CPHA0如果你配成了CPHA1数据在第二个边沿采样就会错位。解决用示波器看SCK和MOSI确认在SCK上升沿时MOSI稳定。另外检查CS是否在发数据前拉低、发完后拉高如果CS一直低芯片可能把多个帧当成一个长帧导致数据错位。4.2 输出有规律纹波或跳动现象输出直流电压上叠加了几十毫伏的纹波频率和SPI时钟或系统时钟相关。原因一般是VREF引脚没有足够去耦或者VDD和VREF共用同一个3.3V且没有隔离。解决VREF单独接基准芯片VDD和VREF各加0.1uF和10uF电容布局时电容尽量靠近引脚。如果纹波频率是50Hz或100Hz那可能是电源纹波检查你的3.3V LDO是否足够干净。4.3 多通道更新时通道错位现象想更新通道3结果通道2变了。原因通常是命令字节的通道地址算错比如把channel直接当命令字节发了没有左移4位。解决确认命令字节 (channel 4) | mode。另外如果你用了级联注意数据发送顺序先发的数据会被推到后面的芯片所以先发最后一片的数据。4.4 SPI发送阻塞导致系统卡顿现象调用SGM5348_Write后系统响应变慢或者中断被延迟。原因HAL_SPI_Transmit是阻塞式发送9MHz下发3字节大概2.7微秒但如果SPI被其他任务占用或者超时时间设得太长就会阻塞。解决如果DAC更新不频繁阻塞没问题如果频繁更新改用HAL_SPI_Transmit_IT或DMA把CS控制放到发送完成回调里。注意用中断方式时CS拉高要在回调里做否则会在数据发完前拉高。4.5 上电瞬间输出尖峰现象系统上电时DAC输出端出现一个短时尖峰可能损坏后级电路。原因SGM5348-12上电时输出寄存器默认是0但输出缓冲器使能瞬间可能有毛刺。解决在输出端加一个RC滤波或者用一颗模拟开关在上电稳定后再接通负载。另外可以在初始化时先写0到所有通道再写目标值避免上电默认值不确定。5. 进阶技巧用DMA和定时器做多通道同步输出与波形生成如果你要做的不只是设置静态电压而是生成波形比如正弦波、三角波那SGM5348-12的8个通道可以派上大用场。一个典型的场景是用STM32F103的定时器触发DMADMA把预先算好的波形数据表依次搬到SPI数据寄存器SPI自动发到DAC这样CPU几乎不参与输出频率可以做到几十kHz。具体做法是先把一个周期的波形采样成N个点每个点12位存到数组里。然后配置TIM2或TIM3设置更新频率 采样率。DMA配置成SPI_TX模式源地址是波形数组目标地址是SPI1-DR传输长度是N循环模式。每次定时器更新事件触发一次DMA请求DMA搬一个16位数据到SPI但SPI是8位帧所以需要把12位数据拆成两个8位或者用SPI的16位数据帧模式。F103的SPI支持16位数据帧但SGM5348-12需要24位帧所以16位模式不够。我一般用DMA搬3个字节到SPI但DMA的传输单位是字节所以波形数组要存成3字节一组每组对应一个采样点。这样DMA每次搬3个字节SPI发3个字节CS在DMA传输期间保持低电平传输完成后拉高。CS的控制可以用定时器的另一个通道或者DMA传输完成中断来做。// 波形数组每个点3字节命令数据高4位数据低8位 // 假设只更新通道0命令字节0x01 uint8_t wave_table[WAVE_LEN * 3]; void Wave_Init(void) { for (int i 0; i WAVE_LEN; i) { float v 1.25f 1.25f * sinf(2 * 3.14159f * i / WAVE_LEN); // 0~2.5V正弦 uint16_t code (uint16_t)(v / 2.5f * 4095); wave_table[i*3] 0x01; // 通道0写并更新 wave_table[i*31] (code 8) 0x0F; wave_table[i*32] code 0xFF; } // 配置DMA和定时器此处省略具体寄存器配置 }这段代码生成一个正弦波表每个点3字节。DMA配置成循环模式定时器触发频率决定输出频率。注意SPI的发送速率要大于定时器触发频率乘以24否则DMA会溢出。比如定时器触发100kHzSPI时钟9MHz发24位需要2.67微秒100kHz周期是10微秒余量足够。如果要做多通道同步可以在波形表里交替写不同通道的命令字节DMA会依次发出实现多通道时分复用输出。这个技巧在做可编程三相电源或LED调光时很实用但要注意DAC的建立时间SGM5348-12的建立时间典型值是6微秒所以输出频率不要超过几十kHz否则波形会失真。我自己的习惯是先用静态电压把驱动调通再用示波器看阶跃响应确认建立时间符合预期最后才上DMA波形。每次改SPI分频或DMA配置都要重新测一遍输出因为时序余量可能被吃掉。希望帮到你。本文还有配套的精品资源点击获取