This project uses the timers of a TI Hercules microcontroller to generate complex digital signals and to measure digital signals.
In this post: create a triangle wave by modulating the duty cycle of a fixed frequency pulse wave.
|
This project is based on TI's application note spna178: Monitoring PWM Using N2HET. That note explains the functionality and has a link to a Code Composer Studio project . I'm using these as the source for the blog series. |
Triangle signal from a pulse wave
Creating a triangle wave by playing with the duty cycle of a pulse wave is a nice simple case.
We're trying to generate an analog signal purely by controlling the amount of energy in each cycle of the pulse wave.
This is different than ladder DACs that generate a level based on a digital value. Our example creates a level by controlling how long a pulse stays high vs low.
When a 3.3 V pulse has 100% duty cycle, the average delivered is 3.3 V.
When that same signal has a duty cycle of 50%, average = 1.65 V, and so forth down to 0 V at 0% duty cycle.
The output signal doesn't look like a triangle when you probe it, because it's a pulse train. When you run it through a low-pass filter, it's integrated into its analog representation: a triangle wave.
I didn't find a good picture with a triangle wave. The below one shows the very same concept but with a sinus signal generated with duty cycle modulation.
On popular demand I'll probe the actual pulse train together with the triangle wave on my launchpad.
source: part of a diagram from TI application note SPNA217: Sine Wave Generation Using PWM With Hercules N2HET
TI has application notes solely focused on generating different wave types (trapezoid, triangle in one and in another note sine wave) by playing with the duty cycle of the HET modules.
They do a much better attempt to explain it than I do here. Check the app notes if the above doesn't make sense.
The HET Code
The flow that generates the pulse train is shown in the chart below:
source: TI's application note spna178: Monitoring PWM Using N2HET, showing the triangle waveform logic
Now the HET code that implements this. I believe this is among the most difficult code to read (together with TI' PRU instructions).
I need the comments to recognise the flowchart parts above.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; This example code is to be loaded to N2HET1 to generate a triangle waveform using varying ; duty cycle ramping from 0% duty to a programmable maximum duty cycle and then ramp down ; to 0% again. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; PWM frequency to be generated PWM_PERIOD .equ 3 ; The pin number that will output the PWM signal PWM_PIN_NUM .equ 9 ; The initial maximum duty cycle (compare value) to be generated. INIT_COMPARE .equ 1 ; delay amount to increment from one duty cycle to the next duty cycle. INCREMEMT_DELAY .equ 0 ; delay amount to decrement from one duty cycle to the next duty cycle. DECREMEMT_DELAY .equ 0 ; amount of increment DUTY_INCREMEMT .equ 0 ; amount of decrement DUTY_DECREMEMT .equ 0 ; amount of increment DUTY_INCREMEMT_HR .equ 1 ; amount of decrement DUTY_DECREMEMT_HR .equ 1 ; key to unlock N2HET UNLOCK_KEY .equ 0xA ; The data field of the MOV32 instruction contains an initial value (0x5) that ; is not equal to the key to unlock the N2HET program. First the MOV32 ; instruction moves the initial value toa temporaray register T L00 MOV32 { remote=DUMMY,type=IMTOREG,reg=T,data=0x5}; ; Compare the register T value with the key to unlock N2HET. The key to unlock ; is 0xA. If the key is not matched then go back to L00. The CPU is supposed ; to write the proper key (0xA) to unlock the N2HET L01 ECMP { next=L00,hr_lr=LOW,cond_addr=L02,pin=0,reg=T,data=UNLOCK_KEY}; ; Creating a virtual counter using CNT which will determines the period of ; the PWM to be generated. The initial small max count allows for quick ; simulation which can later be changed by the host CPU. L02 CNT { reg=A,irq=ON,max=PWM_PERIOD}; ; Use ECMP to determine the duty cycle of the PWM on the specified pin. The ; pin field and the duty cycle are changeable by the CPU. L03 ECMP { hr_lr=HIGH,en_pin_action=ON,cond_addr=L04,pin=PWM_PIN_NUM,action=PULSELO, reg=A,irq=OFF,data=0,hr_data=0}; ; Only when the CNT reaches the max count will the program go to the ; conditional address. We want to wait for one complete PWM waveform to be ; generated before changing the duty cycle. When CNT reaches the max ; value it will set the Z flag. L04 BR { next=L00,cond_addr=L05,event=Z}; ; the data field in this ADD acts as a up/down flag. We want to create a ; triangle waveform. The PWM will first increase the duty cycle until it ; reaches the specified maximum duty cycle before it starts to decrease the ; duty. The up/down flag is used to create two different paths in the flow ; to alternate before increasing duty cycle vs decreasing duty cycle. L05 ADD { src1=ZERO,src2=ZERO,dest=NONE,data=0}; ; Move the up/down flag to a temp register T. L06 MOV32 { remote=L05,type=REMTOREG,reg=T}; ; Compare this up/down flag to 0. 0 means to increase the duty cycle and 1 ; means to decrease the duty cycle. L07 ECMP { next=L16,cond_addr=L08,pin=0,reg=T,data=0}; ; move the ECMP DF which contains the compare value for duty cycle creation ; to register R L08 MOV32 { remote=L03,type=REMTOREG,reg=R}; ; Subtract the current compare value from the max duty cycle stored in ; REM_DUTY. The result will be stored in register S. L09 SUB { src1=REM,src2=R,dest=S,remote=REM_DUTY,data=0}; ; If the substraction result is more than 0 then it means it has not ; reached the max duty cycle we will increase the duty cycle. If it is ; zero then we have reached the max duty cycle and we will change to ; up/down flag to down position. L10 BR { next=L14,cond_addr=L11,event=GT}; ; Insert delay before changing to the next duty cycle L11 DJZ { next=L00,cond_addr=L12,reg=NONE,data=INCREMEMT_DELAY}; ; Add specified amount to the existing compare value (duty cycle). This ; value is also changeable by CPU L12 ADD { src1=R,src2=IMM,dest=S,rdest=REM,remote=L03,data=DUTY_INCREMEMT, hr_data=DUTY_INCREMEMT_HR}; ; Reset the increment delay to the specified amount. L13 MOV32 { next=L15,remote=L11,type=IMTOREG&REM,reg=NONE,data=INCREMEMT_DELAY}; ; Now change the up/down flag to down by moving a 1 to the up/down flag L14 MOV32 { remote=L05,type=IMTOREG&REM,reg=NONE,data=1}; ; Branch to the beginning L15 BR { next=L00,cond_addr=L00,event=NOCOND}; ; move the ECMP DF to register R which contains the current compare value ; (duty cycle) L16 MOV32 { remote=L03,type=REMTOREG,reg=R}; ; Subtract the current duty cycle by the specified amount. This value is ; also changeable by CPU. L17 SUB { src1=R,src2=IMM,dest=S,rdest=NONE,data=DUTY_DECREMEMT, hr_data=DUTY_DECREMEMT_HR}; ; As long as the subtraction result is greater than zero, we will keep ; decreasing the duty cyle or otherwise we will again change the up/down ; flag to up position. The destination register is A which contains the ; subtraction result. L18 BR { next=L19,cond_addr=L22,event=N}; ; Insert the delay before decreasing to the next duty cycle. L19 DJZ { next=L00,cond_addr=L20,reg=NONE,data=DECREMEMT_DELAY}; ; Move the subtraction result to the ECMP DF as the new duty cycle L20 MOV32 { next=L21,remote=L03,type=REGTOREM,reg=S}; ; Reset the decrement delay to the specified amount L21 MOV32 { next=L00,remote=L19,type=IMTOREG&REM,reg=NONE,data=DECREMEMT_DELAY}; ; Move the value 0 to the up/down flag so in the next LRP the program ; flow will execute the path to increase duty cycle. L22 MOV32 { remote=L05,type=IMTOREG&REM,reg=NONE,data=0}; ; Branch to beginning L23 BR { next=L00,cond_addr=L00,event=NOCOND}; ; REM_DUTY data field stores the maximum duty cycle the PWM to be generated. ; The host CPU can change this value. REM_DUTY ECMP { next=REM_DUTY,cond_addr=REM_DUTY,pin=0,reg=A,data=INIT_COMPARE,hr_data=0}; DUMMY BR { next=DUMMY,cond_addr=DUMMY,event=NOCOND,irq=OFF};
Format is label-command-attributes. All instructions are documented in the TMS reference manual, section 17.5.
The defines in the code are placeholders in this example. All of these can be set from the firmware before the timer is kicked off ...
Give data to the HET controller at runtime from Firmware
Here's an example on how these values are overruled at runtime:
/* Change to desired PWM Period in terms of (uS) * the default 45.52uS will generate a 12-bit resolution on the PWM * with each resolution equal to HR=VCLK2=11.11nS. * 45.52uS / 11.11ns = 4096. */ #define PWM_PERIOD 45.52F /* Change to desired maximum duty in terms of (%) * N2HET1 will generate a varying PWM from 0% duty to * the maximum duty specified by PWM_DUTY */ #define PWM_DUTY 100.0 /* allowable LR Prescaler values are 5, 6 and 7. Anything less will * will not have enough time slots for the N2HET program. HR prescaler * is always divide by 1 from VCLK2. * 7 -> one LR = 128 HR * 6 -> one LR = 64 HR * 5 -> one LR = 32 HR */ #define LRPFC 7 /* The NH2ET1 program will automatically increase the PWM * modulation from 0% duty cycle to maximum duty cycle * specified in PWM_DUTY. When PWM_DUTY is reached it starts * to decrease the duty cycle from PWM_DUTY to 0%. * DUTY_INCREMENT specifies the delta amount of duty cycle to * change from one duty cycle to the next duty cycle while * the duty cycle is increasing. This is expressed in terms * of (%). For example specifying DUTY_INCREMENT equal to * 2 will mean the duty cycle will start at 0% and the next * duty cycle will be 2% at a 2% increment. If 0 is * specified, then the N2HET1 will increment the duty cycle * at 1 HR (High Resolution) clock */ #define DUTY_INCREMENT 0.0F /* DUTY_INCREMEMT specifies the delta amount of duty cycle to * change from one duty cycle to the next duty cycle while * the duty cycle is decreasing. This is expressed in terms * of (%). */ #define DUTY_DECREMENT 0.0F /* The increment delay is the delay at which the PWM modulation * will increase the duty cycle from one duty cycle to the * next duty cycle. This is expressed in terms * of (uS). For example, if INCREMENT_DELAY is specified for * 10.0F (equal to 10uS) then the N2HET1 will wait for 10uS * before changing to the new duty cycle */ #define INCREMENT_DELAY 0.0F /* Decrement delay. The Decrement delay is the delay at * which the PWM will decrease the duty cycle. This is expressed * in terms of (uS). */ #define DECREMENT_DELAY 0.0F /* Pin number in N2HET1 to generate the PWM. */ #define NHET1_PIN_PWM PIN_HET_9 // ... /* Set N2HET1[9] to output */ hetREG1->DIR = 1 << NHET1_PIN_PWM; /* Change the LRPFC according to user input */ hetREG1->PFR = (LRPFC << 8) ; /* Initiailize the PWM period and duty cycle based on the defined parameters */ hetRAM1->Instruction[pHET_L02_0].Control = (uint32)CNT_MAX_PERIOD | (hetRAM1->Instruction[pHET_L02_0].Control & 0xFD0000); hetRAM1->Instruction[pHET_REM_DUTY_0].Data = ecmp_compare_value; /* Configure the N2HET1 pin to output the PWM */ hetRAM1->Instruction[pHET_L03_0].Control = (hetRAM1->Instruction[pHET_L03_0].Control & 0xFFFFE0FF) | (NHET1_PIN_PWM << 8); hetRAM1->Instruction[pHET_L12_0].Control = (hetRAM1->Instruction[pHET_L12_0].Control & 0xFFFFE0FF) | (NHET1_PIN_PWM << 8); /* Configure the amount of delay before increasing to the next duty cycle */ hetRAM1->Instruction[pHET_L11_0].Data = ((uint32)RAMPUP_WAIT << 7); hetRAM1->Instruction[pHET_L13_0].Data = ((uint32)RAMPUP_WAIT << 7); /* Configure the amount of delay before decreasing to the next duty cycle */ hetRAM1->Instruction[pHET_L19_0].Data = ((uint32)RAMPDOWN_WAIT << 7); hetRAM1->Instruction[pHET_L21_0].Data = ((uint32)RAMPDOWN_WAIT << 7); /* Duty cycle increment amount */ if ( (uint32)DELTA_INCREMENT == 0){ hetRAM1->Instruction[pHET_L12_0].Data = (1 << (7 - LRPFC)); } else { hetRAM1->Instruction[pHET_L12_0].Data = (((uint32)DELTA_INCREMENT) << (7 - LRPFC)); } /* Duty cycle decrement amount */ if ((uint32)DELTA_DECREMENT == 0) { hetRAM1->Instruction[pHET_L17_0].Data = (1 << (7 - LRPFC)); } else { hetRAM1->Instruction[pHET_L17_0].Data = (((uint32)DELTA_DECREMENT) << (7 - LRPFC)); } /* Unlock the N2HET program. Initially after reset the N2HET program is locked */ hetRAM1->Instruction[pHET_L00_0].Data = UNLOCK_KEY << 7;
The way of working is basic. The HET assembler translates your HET program in an array with binary data. One entry per instruction.
But it helps you a little with a custom that fits your code and allows you to see where the parameters are for each instruction, so that you don't have to perform magic logic.
The assembler instruction (as discussed in the previous post , this command is entered in the project pre-build steps:
${HET_COMPILER} -v2 -n0 -hc32 "..\HET_code\Async_PWM_Triangle_Wave.het"
The easiest way to get your head around this is by using the flow chart and HET code side by side and trying to follow the logic.
This is doable. Much more difficult (to me) is writing a significant HET program from scratch. That's a challenge I'm going to battle this year.
Related Blog |
---|
1: adapt TI example to a LaunchPad |
2: Generate Dynamic Duty Cycle |
3: Measure a PWM Signal and HET Interrupts |
4: Getting Started |
5: Getting Started with the Wave Form Simulator |
Top Comments